Changeset 0.25.2 (#162)
This commit is contained in:
@@ -101,6 +101,7 @@
|
|||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/file-tree.js?v={{.Version}}"></script>
|
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/file-tree.js?v={{.Version}}"></script>
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/code-editor.js?v={{.Version}}"></script>
|
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/code-editor.js?v={{.Version}}"></script>
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/note-editor.js?v={{.Version}}"></script>
|
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/note-editor.js?v={{.Version}}"></script>
|
||||||
|
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/drag-resize.js?v={{.Version}}"></script>
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/pane-container.js?v={{.Version}}"></script>
|
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/pane-container.js?v={{.Version}}"></script>
|
||||||
|
|
||||||
{{if eq .Surface "chat"}}{{template "scripts-chat" .}}{{end}}
|
{{if eq .Surface "chat"}}{{template "scripts-chat" .}}{{end}}
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
|
|
||||||
{{/* Top Bar */}}
|
{{/* Top Bar */}}
|
||||||
<div class="admin-topbar">
|
<div class="admin-topbar">
|
||||||
<a href="{{.BasePath}}/" class="admin-topbar-back">
|
<a href="{{.BasePath}}/" class="admin-topbar-back" onclick="if(history.length>1){history.back();return false;}">
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
|
||||||
Back to Chat
|
Back
|
||||||
</a>
|
</a>
|
||||||
<div class="admin-topbar-sep"></div>
|
<div class="admin-topbar-sep"></div>
|
||||||
<span class="admin-topbar-title"><img src="{{.BasePath}}/favicon.svg?v={{.Version}}" alt="" width="18" height="18" style="vertical-align: -3px;"> Administration</span>
|
<span class="admin-topbar-title"><img src="{{.BasePath}}/favicon.svg?v={{.Version}}" alt="" width="18" height="18" style="vertical-align: -3px;"> Administration</span>
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
|
|
||||||
{{/* Top Bar */}}
|
{{/* Top Bar */}}
|
||||||
<div class="settings-topbar">
|
<div class="settings-topbar">
|
||||||
<a href="{{.BasePath}}/" class="settings-topbar-back">
|
<a href="{{.BasePath}}/" class="settings-topbar-back" onclick="if(history.length>1){history.back();return false;}">
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
|
||||||
Back to Chat
|
Back
|
||||||
</a>
|
</a>
|
||||||
<div class="settings-topbar-sep"></div>
|
<div class="settings-topbar-sep"></div>
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--text-2)"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--text-2)"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/></svg>
|
||||||
|
|||||||
@@ -171,16 +171,13 @@
|
|||||||
|
|
||||||
.msg-inner { max-width: 768px; margin: 0 auto; display: flex; gap: 10px; }
|
.msg-inner { max-width: 768px; margin: 0 auto; display: flex; gap: 10px; }
|
||||||
|
|
||||||
/* User messages: right-aligned bubble */
|
/* User messages: same fixed-width container, mirrored */
|
||||||
.message.user { display: flex; justify-content: flex-end; }
|
|
||||||
.message.user .msg-inner { flex-direction: row-reverse; }
|
.message.user .msg-inner { flex-direction: row-reverse; }
|
||||||
.message.user .msg-body {
|
.message.user .msg-body {
|
||||||
background: var(--user-bubble);
|
background: var(--user-bubble);
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: 16px 16px 4px 16px;
|
border-radius: 16px 16px 4px 16px;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
flex: initial;
|
|
||||||
max-width: 85%;
|
|
||||||
}
|
}
|
||||||
.message.user .msg-head { flex-direction: row-reverse; }
|
.message.user .msg-head { flex-direction: row-reverse; }
|
||||||
.message.user .msg-role { color: var(--accent); }
|
.message.user .msg-role { color: var(--accent); }
|
||||||
|
|||||||
@@ -112,14 +112,6 @@
|
|||||||
.sidebar.collapsed .sb-btn { justify-content: center; padding: 8px 0; gap: 0; }
|
.sidebar.collapsed .sb-btn { justify-content: center; padding: 8px 0; gap: 0; }
|
||||||
.sidebar.collapsed .split-btn-main { justify-content: center; padding: 8px 0; gap: 0; }
|
.sidebar.collapsed .split-btn-main { justify-content: center; padding: 8px 0; gap: 0; }
|
||||||
.sidebar.collapsed .user-btn { justify-content: center; padding: 6px 0; gap: 0; }
|
.sidebar.collapsed .user-btn { justify-content: center; padding: 6px 0; gap: 0; }
|
||||||
.sidebar.collapsed .user-flyout {
|
|
||||||
position: fixed;
|
|
||||||
left: var(--sidebar-rail);
|
|
||||||
bottom: 8px;
|
|
||||||
right: auto;
|
|
||||||
width: 200px;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
.sidebar.collapsed .avatar-bug { display: none; }
|
.sidebar.collapsed .avatar-bug { display: none; }
|
||||||
|
|
||||||
/* Split button (New Chat + dropdown) */
|
/* Split button (New Chat + dropdown) */
|
||||||
|
|||||||
@@ -157,6 +157,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar.collapsed .user-menu-wrap .user-flyout {
|
.sidebar.collapsed .user-menu-wrap .user-flyout {
|
||||||
left: 8px;
|
position: fixed;
|
||||||
right: 8px;
|
left: var(--sidebar-rail);
|
||||||
|
bottom: 8px;
|
||||||
|
top: auto;
|
||||||
|
right: auto;
|
||||||
|
width: 200px;
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,10 +135,12 @@ function wikilinkAutocomplete(linkCompleter) {
|
|||||||
options: results.map(r => ({
|
options: results.map(r => ({
|
||||||
label: r.label || r.title,
|
label: r.label || r.title,
|
||||||
apply: (view, completion, from, to) => {
|
apply: (view, completion, from, to) => {
|
||||||
// Replace query text with completed title + closing ]]
|
// Check if ]] already exists after cursor (from closeBrackets auto-close)
|
||||||
|
const afterCursor = view.state.doc.sliceString(to, to + 2);
|
||||||
|
const endPos = afterCursor === ']]' ? to + 2 : to;
|
||||||
const insert = completion.label + ']]';
|
const insert = completion.label + ']]';
|
||||||
view.dispatch({
|
view.dispatch({
|
||||||
changes: { from, to, insert },
|
changes: { from, to: endPos, insert },
|
||||||
selection: { anchor: from + insert.length },
|
selection: { anchor: from + insert.length },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
94
src/js/drag-resize.js
Normal file
94
src/js/drag-resize.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
// ==========================================
|
||||||
|
// Chat Switchboard — Drag Resize Utility
|
||||||
|
// ==========================================
|
||||||
|
// v0.25.2: Single drag-resize primitive used by both the workspace
|
||||||
|
// handle (panels.js) and PaneContainer split handles (pane-container.js).
|
||||||
|
//
|
||||||
|
// Fixes vs the two prior implementations:
|
||||||
|
// - Full-viewport overlay during drag prevents iframes / CM6 from
|
||||||
|
// swallowing mousemove events (the "jump to middle" bug).
|
||||||
|
// - Touch support (touchstart / touchmove / touchend) everywhere.
|
||||||
|
// - Transitions on dragged elements are killed on grab and restored
|
||||||
|
// only after release — no mid-animation capture of wrong startW.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// initDragResize(handleEl, {
|
||||||
|
// direction: 'horizontal', // or 'vertical'
|
||||||
|
// onStart(clientPos) → ctx | false, // return drag context or false to cancel
|
||||||
|
// onMove(delta, ctx), // delta = px from start (signed)
|
||||||
|
// onEnd(ctx), // cleanup / persist
|
||||||
|
// });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach drag-resize behavior to a handle element.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} handleEl — the drag grip element
|
||||||
|
* @param {object} opts
|
||||||
|
* @param {'horizontal'|'vertical'} opts.direction
|
||||||
|
* @param {function} opts.onStart — receives initial clientX/Y, returns context or false
|
||||||
|
* @param {function} opts.onMove — receives (delta, ctx)
|
||||||
|
* @param {function} opts.onEnd — receives (ctx)
|
||||||
|
* @param {function} [opts.canDrag] — optional guard; return false to ignore mousedown
|
||||||
|
*/
|
||||||
|
function initDragResize(handleEl, opts) {
|
||||||
|
if (!handleEl) return;
|
||||||
|
|
||||||
|
const isHoriz = (opts.direction || 'horizontal') === 'horizontal';
|
||||||
|
|
||||||
|
const onPointerDown = (e) => {
|
||||||
|
// Optional guard (e.g. "only when .active class is present")
|
||||||
|
if (opts.canDrag && !opts.canDrag()) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
const clientPos = _clientPos(e, isHoriz);
|
||||||
|
if (clientPos == null) return;
|
||||||
|
|
||||||
|
const ctx = opts.onStart(clientPos);
|
||||||
|
if (ctx === false) return;
|
||||||
|
|
||||||
|
// ── Overlay: blocks iframes / CodeMirror from eating events ──
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.style.cssText =
|
||||||
|
'position:fixed;inset:0;z-index:99999;' +
|
||||||
|
'cursor:' + (isHoriz ? 'col-resize' : 'row-resize') + ';';
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
document.body.style.cursor = isHoriz ? 'col-resize' : 'row-resize';
|
||||||
|
document.body.style.userSelect = 'none';
|
||||||
|
|
||||||
|
const onMove = (e) => {
|
||||||
|
const pos = _clientPos(e, isHoriz);
|
||||||
|
if (pos == null) return;
|
||||||
|
opts.onMove(pos - clientPos, ctx);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUp = () => {
|
||||||
|
overlay.remove();
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
document.body.style.userSelect = '';
|
||||||
|
document.removeEventListener('mousemove', onMove);
|
||||||
|
document.removeEventListener('mouseup', onUp);
|
||||||
|
document.removeEventListener('touchmove', onMove);
|
||||||
|
document.removeEventListener('touchend', onUp);
|
||||||
|
if (opts.onEnd) opts.onEnd(ctx);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onMove);
|
||||||
|
document.addEventListener('mouseup', onUp);
|
||||||
|
document.addEventListener('touchmove', onMove, { passive: false });
|
||||||
|
document.addEventListener('touchend', onUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleEl.addEventListener('mousedown', onPointerDown);
|
||||||
|
handleEl.addEventListener('touchstart', onPointerDown, { passive: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Extract the relevant coordinate from a mouse or touch event. */
|
||||||
|
function _clientPos(e, isHoriz) {
|
||||||
|
if (e.touches?.length > 0) {
|
||||||
|
return isHoriz ? e.touches[0].clientX : e.touches[0].clientY;
|
||||||
|
}
|
||||||
|
if (e.changedTouches?.length > 0) {
|
||||||
|
return isHoriz ? e.changedTouches[0].clientX : e.changedTouches[0].clientY;
|
||||||
|
}
|
||||||
|
return isHoriz ? e.clientX : e.clientY;
|
||||||
|
}
|
||||||
@@ -19,8 +19,6 @@ async function openNotes() {
|
|||||||
}
|
}
|
||||||
PanelRegistry.open('notes');
|
PanelRegistry.open('notes');
|
||||||
_exitSelectMode();
|
_exitSelectMode();
|
||||||
await loadNotesList();
|
|
||||||
await loadNoteFolders();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadNotesList(folder, searchQuery) {
|
async function loadNotesList(folder, searchQuery) {
|
||||||
@@ -603,6 +601,10 @@ async function saveMessageToNote(msgIndex) {
|
|||||||
// Check for text selection within the message element
|
// Check for text selection within the message element
|
||||||
const sel = window.getSelection();
|
const sel = window.getSelection();
|
||||||
let content = msg.content;
|
let content = msg.content;
|
||||||
|
|
||||||
|
// Strip thinking/reasoning blocks — only the response matters in notes
|
||||||
|
content = content.replace(/<(?:thinking|think)>[\s\S]*?<\/(?:thinking|think)>/gi, '').trim();
|
||||||
|
|
||||||
const msgId = msg.id || '';
|
const msgId = msg.id || '';
|
||||||
const msgEl = msgId ? document.querySelector(`.message[data-msg-id="${msgId}"] .msg-text`) : null;
|
const msgEl = msgId ? document.querySelector(`.message[data-msg-id="${msgId}"] .msg-text`) : null;
|
||||||
if (sel && sel.rangeCount > 0 && msgEl?.contains(sel.anchorNode)) {
|
if (sel && sel.rangeCount > 0 && msgEl?.contains(sel.anchorNode)) {
|
||||||
@@ -820,7 +822,18 @@ function _registerNotesPanel() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Graph view -->
|
<!-- Graph view -->
|
||||||
<div id="notesGraphView" style="display:none;"></div>`;
|
<div id="notesGraphView" class="notes-graph-view" style="display:none;">
|
||||||
|
<div class="notes-graph-toolbar">
|
||||||
|
<button class="btn-small" id="notesGraphBackBtn" title="Back to list">← Back</button>
|
||||||
|
<span class="notes-graph-stats">
|
||||||
|
<span id="graphNodeCount">0</span> notes · <span id="graphEdgeCount">0</span> links
|
||||||
|
</span>
|
||||||
|
<div style="flex:1"></div>
|
||||||
|
<button class="btn-small" onclick="_graphToggleOrphans()" title="Toggle orphan nodes">Orphans</button>
|
||||||
|
<button class="btn-small" onclick="_graphResetZoom()" title="Reset zoom">Reset</button>
|
||||||
|
</div>
|
||||||
|
<canvas id="noteGraphCanvas" style="width:100%;display:block;"></canvas>
|
||||||
|
</div>`;
|
||||||
body.appendChild(el);
|
body.appendChild(el);
|
||||||
|
|
||||||
// ── Wire listeners ──
|
// ── Wire listeners ──
|
||||||
@@ -833,6 +846,9 @@ function _registerNotesPanel() {
|
|||||||
if (typeof openNoteGraph === 'function') openNoteGraph();
|
if (typeof openNoteGraph === 'function') openNoteGraph();
|
||||||
});
|
});
|
||||||
el.querySelector('#notesTodayBtn').addEventListener('click', openDailyNote);
|
el.querySelector('#notesTodayBtn').addEventListener('click', openDailyNote);
|
||||||
|
el.querySelector('#notesGraphBackBtn').addEventListener('click', () => {
|
||||||
|
if (typeof closeNoteGraph === 'function') closeNoteGraph();
|
||||||
|
});
|
||||||
el.querySelector('#notesSelectModeBtn').addEventListener('click', () => {
|
el.querySelector('#notesSelectModeBtn').addEventListener('click', () => {
|
||||||
if (_notesSelectMode) _exitSelectMode();
|
if (_notesSelectMode) _exitSelectMode();
|
||||||
else _enterSelectMode();
|
else _enterSelectMode();
|
||||||
@@ -890,7 +906,9 @@ function _registerNotesPanel() {
|
|||||||
element: el,
|
element: el,
|
||||||
label: 'Notes',
|
label: 'Notes',
|
||||||
onOpen() {
|
onOpen() {
|
||||||
// Loading is handled by openNotes() which calls loadNotesList
|
// Ensure notes are loaded whenever the panel opens
|
||||||
|
loadNotesList();
|
||||||
|
loadNoteFolders();
|
||||||
},
|
},
|
||||||
onClose() {
|
onClose() {
|
||||||
_destroyNoteEditor();
|
_destroyNoteEditor();
|
||||||
|
|||||||
@@ -345,26 +345,28 @@ function _createHandle(direction, splitEl, index, surfaceId, splitId) {
|
|||||||
splitEl.appendChild(handle);
|
splitEl.appendChild(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drag logic
|
// Track adjacent panes for dblclick reset
|
||||||
let startX, startY, leftEl, rightEl, startLeftBasis, startRightBasis;
|
let lastLeftEl = null, lastRightEl = null;
|
||||||
|
|
||||||
const onMouseDown = (e) => {
|
initDragResize(handle, {
|
||||||
e.preventDefault();
|
direction,
|
||||||
const allPanes = Array.from(splitEl.querySelectorAll(':scope > .pane, :scope > .pane-split'));
|
onStart(_clientPos) {
|
||||||
// Find the two panes adjacent to this handle
|
// Find the two panes adjacent to this handle
|
||||||
const handleIdx = Array.from(splitEl.children).indexOf(handle);
|
const handleIdx = Array.from(splitEl.children).indexOf(handle);
|
||||||
leftEl = splitEl.children[handleIdx - 1];
|
const leftEl = splitEl.children[handleIdx - 1];
|
||||||
rightEl = splitEl.children[handleIdx + 1];
|
const rightEl = splitEl.children[handleIdx + 1];
|
||||||
if (!leftEl || !rightEl) return;
|
if (!leftEl || !rightEl) return false;
|
||||||
|
|
||||||
startX = e.clientX;
|
lastLeftEl = leftEl;
|
||||||
startY = e.clientY;
|
lastRightEl = rightEl;
|
||||||
// Use current rendered size (matches what flex layout computed)
|
|
||||||
startLeftBasis = leftEl.getBoundingClientRect().width;
|
|
||||||
startRightBasis = rightEl.getBoundingClientRect().width;
|
|
||||||
|
|
||||||
// Immediately lock the non-flex panes to their current sizes
|
const isHoriz = direction === 'horizontal';
|
||||||
// so the first move delta doesn't cause a visual snap
|
const dim = isHoriz ? 'width' : 'height';
|
||||||
|
const startLeftBasis = leftEl.getBoundingClientRect()[dim];
|
||||||
|
const startRightBasis = rightEl.getBoundingClientRect()[dim];
|
||||||
|
|
||||||
|
// Lock non-flex panes to current rendered size so the first
|
||||||
|
// move delta doesn't cause a visual snap
|
||||||
if (leftEl.dataset.paneFlex !== '1') {
|
if (leftEl.dataset.paneFlex !== '1') {
|
||||||
leftEl.style.flexBasis = startLeftBasis + 'px';
|
leftEl.style.flexBasis = startLeftBasis + 'px';
|
||||||
leftEl.style.flexGrow = '0';
|
leftEl.style.flexGrow = '0';
|
||||||
@@ -377,37 +379,28 @@ function _createHandle(direction, splitEl, index, surfaceId, splitId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handle.classList.add('pane-handle--active');
|
handle.classList.add('pane-handle--active');
|
||||||
document.body.style.cursor = direction === 'horizontal' ? 'col-resize' : 'row-resize';
|
|
||||||
document.body.style.userSelect = 'none';
|
|
||||||
|
|
||||||
document.addEventListener('mousemove', onMouseMove);
|
|
||||||
document.addEventListener('mouseup', onMouseUp);
|
|
||||||
};
|
|
||||||
|
|
||||||
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 leftIsFlex = leftEl.dataset.paneFlex === '1';
|
||||||
const rightIsFlex = rightEl.dataset.paneFlex === '1';
|
const rightIsFlex = rightEl.dataset.paneFlex === '1';
|
||||||
// Cap: no single fixed pane should exceed 60% of container
|
const isHoriz = direction === 'horizontal';
|
||||||
const maxSize = splitEl.getBoundingClientRect().width * 0.6;
|
const dim = isHoriz ? 'width' : 'height';
|
||||||
|
const maxSize = splitEl.getBoundingClientRect()[dim] * 0.6;
|
||||||
|
|
||||||
if (leftIsFlex) {
|
if (leftIsFlex) {
|
||||||
// Only adjust the right (fixed) pane; left flex absorbs remainder
|
|
||||||
const newRight = Math.min(maxSize, Math.max(50, startRightBasis - delta));
|
const newRight = Math.min(maxSize, Math.max(50, startRightBasis - delta));
|
||||||
rightEl.style.flexBasis = newRight + 'px';
|
rightEl.style.flexBasis = newRight + 'px';
|
||||||
rightEl.style.flexGrow = '0';
|
rightEl.style.flexGrow = '0';
|
||||||
rightEl.style.flexShrink = '0';
|
rightEl.style.flexShrink = '0';
|
||||||
} else if (rightIsFlex) {
|
} else if (rightIsFlex) {
|
||||||
// Only adjust the left (fixed) pane; right flex absorbs remainder
|
|
||||||
const newLeft = Math.min(maxSize, Math.max(50, startLeftBasis + delta));
|
const newLeft = Math.min(maxSize, Math.max(50, startLeftBasis + delta));
|
||||||
leftEl.style.flexBasis = newLeft + 'px';
|
leftEl.style.flexBasis = newLeft + 'px';
|
||||||
leftEl.style.flexGrow = '0';
|
leftEl.style.flexGrow = '0';
|
||||||
leftEl.style.flexShrink = '0';
|
leftEl.style.flexShrink = '0';
|
||||||
} else {
|
} else {
|
||||||
// Both fixed — adjust both (original behavior)
|
|
||||||
const newLeft = Math.max(50, startLeftBasis + delta);
|
const newLeft = Math.max(50, startLeftBasis + delta);
|
||||||
const newRight = Math.max(50, startRightBasis - delta);
|
const newRight = Math.max(50, startRightBasis - delta);
|
||||||
leftEl.style.flexBasis = newLeft + 'px';
|
leftEl.style.flexBasis = newLeft + 'px';
|
||||||
@@ -417,43 +410,34 @@ function _createHandle(direction, splitEl, index, surfaceId, splitId) {
|
|||||||
rightEl.style.flexGrow = '0';
|
rightEl.style.flexGrow = '0';
|
||||||
rightEl.style.flexShrink = '0';
|
rightEl.style.flexShrink = '0';
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
onEnd(_ctx) {
|
||||||
const onMouseUp = () => {
|
|
||||||
handle.classList.remove('pane-handle--active');
|
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);
|
_persistSizes(surfaceId, splitEl);
|
||||||
};
|
},
|
||||||
|
});
|
||||||
handle.addEventListener('mousedown', onMouseDown);
|
|
||||||
|
|
||||||
// Double-click resets to default
|
// Double-click resets to default
|
||||||
handle.addEventListener('dblclick', () => {
|
handle.addEventListener('dblclick', () => {
|
||||||
if (leftEl) {
|
_resetPaneSize(lastLeftEl);
|
||||||
if (leftEl.dataset.paneFlex === '1') {
|
_resetPaneSize(lastRightEl);
|
||||||
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 = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_persistSizes(surfaceId, splitEl);
|
_persistSizes(surfaceId, splitEl);
|
||||||
});
|
});
|
||||||
|
|
||||||
return { handle, splitEl, index, direction };
|
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 ─────────────────────────────
|
// ── Persistence ─────────────────────────────
|
||||||
|
|
||||||
function _persistSizes(surfaceId, splitEl) {
|
function _persistSizes(surfaceId, splitEl) {
|
||||||
|
|||||||
@@ -302,46 +302,37 @@ function toggleSidePanelFullscreen() {
|
|||||||
// ── Workspace Resize Handle ────────────────
|
// ── Workspace Resize Handle ────────────────
|
||||||
|
|
||||||
function _initWorkspaceResize() {
|
function _initWorkspaceResize() {
|
||||||
let startX, startW;
|
|
||||||
const secondary = document.getElementById('workspaceSecondary');
|
const secondary = document.getElementById('workspaceSecondary');
|
||||||
const handle = document.getElementById('workspaceHandle');
|
const handle = document.getElementById('workspaceHandle');
|
||||||
if (!handle || !secondary) return;
|
if (!handle || !secondary) return;
|
||||||
|
|
||||||
const onDown = (e) => {
|
initDragResize(handle, {
|
||||||
if (!handle.classList.contains('active')) return;
|
direction: 'horizontal',
|
||||||
if (secondary.classList.contains('fullscreen')) return;
|
canDrag() {
|
||||||
e.preventDefault();
|
return handle.classList.contains('active') &&
|
||||||
const clientX = e.clientX ?? e.touches?.[0]?.clientX;
|
!secondary.classList.contains('fullscreen');
|
||||||
startX = clientX;
|
},
|
||||||
startW = secondary.getBoundingClientRect().width;
|
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';
|
secondary.style.transition = 'none';
|
||||||
document.body.style.cursor = 'col-resize';
|
void secondary.offsetWidth;
|
||||||
document.body.style.userSelect = 'none';
|
const startW = secondary.getBoundingClientRect().width;
|
||||||
|
return { startW };
|
||||||
const onMove = (e) => {
|
},
|
||||||
const cx = e.clientX ?? e.touches?.[0]?.clientX ?? startX;
|
onMove(delta, ctx) {
|
||||||
const delta = startX - cx;
|
// Handle is left of secondary — dragging left (negative delta) grows it
|
||||||
const newW = Math.max(280, Math.min(window.innerWidth * 0.7, startW + delta));
|
const newW = Math.max(280, Math.min(window.innerWidth * 0.7, ctx.startW - delta));
|
||||||
secondary.style.width = newW + 'px';
|
secondary.style.width = newW + 'px';
|
||||||
secondary.style.minWidth = newW + 'px';
|
secondary.style.minWidth = newW + 'px';
|
||||||
};
|
},
|
||||||
const onUp = () => {
|
onEnd(_ctx) {
|
||||||
document.removeEventListener('mousemove', onMove);
|
|
||||||
document.removeEventListener('mouseup', onUp);
|
|
||||||
document.removeEventListener('touchmove', onMove);
|
|
||||||
document.removeEventListener('touchend', onUp);
|
|
||||||
document.body.style.cursor = '';
|
|
||||||
document.body.style.userSelect = '';
|
|
||||||
secondary.style.transition = '';
|
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 ─────────────────
|
// ── Mobile Swipe Navigation ─────────────────
|
||||||
|
|||||||
@@ -153,6 +153,11 @@ async function deleteProject(projectId) {
|
|||||||
App.activeProjectId = null;
|
App.activeProjectId = null;
|
||||||
localStorage.removeItem('cs-active-project');
|
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.renderChatList();
|
||||||
UI.toast('Project deleted', 'success');
|
UI.toast('Project deleted', 'success');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user