experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor(editor): redesign pane management with uniform state model

Replace scattered, inconsistent pane visibility/collapse logic with a
single unified system. All four panes (outline, editor, preview, notes)
now use identical state management:

- Two independent states per pane: visible (bool) and collapsed (bool)
- Layout owns ALL state; sidebar components only reflect visual appearance
- _applyPaneState() is the single source of truth for DOM updates
- Toolbar toggles cycle: expanded -> collapsed -> expanded (never hides)
- Resizing only affects the non-editor pane, persists width to localStorage
- Focus mode saves/restores complete pane state
- Collapsed state clears inline width/minWidth to guarantee 32px strip

+298 -363
+30 -49
extensions/editor/annotations-pane.js
··· 10 10 this.onAnnotationClick = options.onAnnotationClick; // (highlight) => void 11 11 this.onAnnotationDelete = options.onAnnotationDelete; // (highlightId) => void 12 12 this.onAnnotationEdit = options.onAnnotationEdit; // (highlightId, note) => void 13 - this.onAddNote = options.onAddNote; // () => void — trigger highlight from selection 14 - this.onToggle = options.onToggle; // () => void — called when internal toggle clicked 15 - this.collapsed = true; // Start collapsed 13 + this.onToggle = options.onToggle; // callback: () => void — layout handles state 14 + this.collapsed = false; // Layout owns this state 16 15 this.highlights = []; 17 16 18 17 this.element = document.createElement('div'); 19 - this.element.className = 'annotations-pane collapsed'; 18 + this.element.className = 'annotations-pane'; 20 19 21 20 // Header 22 21 this.header = document.createElement('div'); ··· 24 23 25 24 const title = document.createElement('span'); 26 25 title.className = 'sidebar-title'; 27 - title.textContent = 'Notes'; 26 + title.textContent = 'Annotations'; 28 27 this.header.appendChild(title); 29 28 30 29 const countBadge = document.createElement('span'); ··· 33 32 this.countBadge = countBadge; 34 33 this.header.appendChild(countBadge); 35 34 36 - // Add Note button 37 - this.addNoteBtn = document.createElement('button'); 38 - this.addNoteBtn.className = 'annotation-add-btn'; 39 - this.addNoteBtn.textContent = '+'; 40 - this.addNoteBtn.title = 'Add note at selection (Cmd+Shift+L)'; 41 - this.addNoteBtn.tabIndex = -1; 42 - this.addNoteBtn.addEventListener('mousedown', (e) => e.preventDefault()); 43 - this.addNoteBtn.addEventListener('click', () => { 44 - if (this.onAddNote) this.onAddNote(); 45 - }); 46 - this.header.appendChild(this.addNoteBtn); 47 - 48 35 this.toggleBtn = document.createElement('button'); 49 36 this.toggleBtn.className = 'sidebar-toggle'; 50 37 this.toggleBtn.innerHTML = '\u25B6'; // > 51 38 this.toggleBtn.tabIndex = -1; 52 39 this.toggleBtn.addEventListener('mousedown', (e) => e.preventDefault()); 53 - this.toggleBtn.addEventListener('click', () => { 54 - if (this.onToggle) { 55 - this.onToggle(); 56 - } else { 57 - this.toggle(); 58 - } 59 - }); 40 + this.toggleBtn.addEventListener('click', () => this.toggle()); 60 41 this.header.appendChild(this.toggleBtn); 61 42 62 43 this.element.appendChild(this.header); ··· 72 53 /** 73 54 * Update the displayed highlights. 74 55 * @param {Array} highlights - Array of {id, from, to, text, note, color} 75 - * @param {object} [doc] - CodeMirror doc for line number lookup 76 56 */ 77 - update(highlights, doc) { 57 + update(highlights) { 78 58 this.highlights = highlights || []; 79 - this.doc = doc || this.doc; 80 59 this.countBadge.textContent = String(this.highlights.length); 81 60 this.render(); 82 61 } ··· 87 66 if (this.highlights.length === 0) { 88 67 const empty = document.createElement('div'); 89 68 empty.className = 'annotations-empty'; 90 - empty.innerHTML = 'No notes yet.<br><br>Select text or place cursor, then click <strong>+</strong> or press <strong>\u2318\u21E7L</strong> to add a note.'; 69 + empty.textContent = 'No highlights yet. Select text and use Cmd+H to highlight.'; 91 70 this.content.appendChild(empty); 92 71 return; 93 72 } ··· 105 84 colorDot.style.background = hl.color || 'var(--base0A)'; 106 85 item.appendChild(colorDot); 107 86 108 - // Line reference + user note 87 + // Text content 109 88 const textWrap = document.createElement('div'); 110 89 textWrap.className = 'annotation-text-wrap'; 111 90 112 - // Show line number 113 - const lineRef = document.createElement('div'); 114 - lineRef.className = 'annotation-text'; 115 - let lineNum = '?'; 116 - try { 117 - if (this.doc) lineNum = this.doc.lineAt(hl.from).number; 118 - } catch (e) { /* position may be stale */ } 119 - lineRef.textContent = hl.note ? `Line ${lineNum}` : `Line ${lineNum} — highlight`; 120 - textWrap.appendChild(lineRef); 91 + const text = document.createElement('div'); 92 + text.className = 'annotation-text'; 93 + text.textContent = hl.text.length > 80 ? hl.text.substring(0, 80) + '...' : hl.text; 94 + textWrap.appendChild(text); 121 95 122 96 if (hl.note) { 123 97 const note = document.createElement('div'); ··· 134 108 135 109 const editBtn = document.createElement('button'); 136 110 editBtn.className = 'annotation-btn'; 137 - editBtn.textContent = 'Edit'; 138 - editBtn.title = 'Edit note text'; 111 + editBtn.textContent = 'Note'; 112 + editBtn.title = 'Edit note'; 139 113 editBtn.addEventListener('mousedown', (e) => e.preventDefault()); 140 114 editBtn.addEventListener('click', (e) => { 141 115 e.stopPropagation(); ··· 177 151 } 178 152 } 179 153 180 - toggle() { 181 - this.setCollapsed(!this.collapsed); 182 - } 183 - 154 + /** 155 + * Set collapsed visual state. Called by layout — pane does NOT own this state. 156 + */ 184 157 setCollapsed(collapsed) { 185 158 this.collapsed = collapsed; 186 - this.element.classList.toggle('collapsed', this.collapsed); 187 - 188 - if (this.collapsed) { 159 + this.element.classList.toggle('collapsed', collapsed); 160 + if (collapsed) { 189 161 this.toggleBtn.innerHTML = '\u25B6'; 190 - this.toggleBtn.title = 'Show Notes'; 162 + this.toggleBtn.title = 'Show Annotations'; 191 163 } else { 192 164 this.toggleBtn.innerHTML = '\u25C0'; 193 - this.toggleBtn.title = 'Hide Notes'; 165 + this.toggleBtn.title = 'Hide Annotations'; 166 + } 167 + } 168 + 169 + /** 170 + * Toggle (for internal toggle button click) — delegates to layout. 171 + */ 172 + toggle() { 173 + if (this.onToggle) { 174 + this.onToggle(); 194 175 } 195 176 } 196 177
+219 -211
extensions/editor/editor-layout.js
··· 1 1 /** 2 2 * Editor Layout - Multi-panel markdown editor with pane management. 3 3 * 4 - * Features: 5 - * - Three main panes: outline, editor, preview (independently togglable) 6 - * - Fourth annotations pane anchored to far right 4 + * Pane management model: 5 + * - Each pane has two independent states: visible (bool) and collapsed (bool) 6 + * - Visible+expanded: shows at persisted width (or CSS default) 7 + * - Visible+collapsed: 32px strip with header only 8 + * - Hidden (!visible): display:none, not shown at all 9 + * - Toolbar buttons toggle collapsed state (expanded <-> collapsed) 10 + * - Hidden state is only for programmatic use (annotations default, focus mode) 11 + * - Layout owns ALL state; sidebar components only update their visual appearance 12 + * - _applyPaneState() is the single source of truth for DOM 13 + * 14 + * Other features: 7 15 * - Pane ordering via toolbar controls (persisted) 8 - * - Pane resizing with drag handles, double-click to maximize/minimize 16 + * - Pane resizing with drag handles 9 17 * - Preview scroll sync with editor 10 18 * - Persistent highlights with annotations 11 19 */ ··· 24 32 } from './highlights.js'; 25 33 26 34 const LAYOUT_PREFS_KEY = 'editor:layout-prefs'; 35 + 36 + /** Default widths for each pane when expanded (CSS values). */ 37 + const DEFAULT_WIDTHS = { 38 + outline: '220px', 39 + preview: '400px', 40 + annotations: '260px', 41 + }; 27 42 28 43 /** 29 44 * Load layout preferences from localStorage. ··· 62 77 this.rafId = null; 63 78 this.isDestroyed = false; 64 79 this.isFocusMode = false; 65 - this.originalContainerStyles = ''; 80 + this.preFocusState = null; // saved state before focus mode 66 81 this.cmdSuggestionsCleanup = null; 67 82 68 - // Pane state — outline and preview start collapsed 69 - this.paneVisibility = { outline: true, editor: true, preview: true, annotations: false }; 70 - this.paneCollapsed = { outline: true, preview: true }; 83 + // Pane state — uniform for all four panes 84 + // visible: whether the pane is shown at all (hidden = display:none) 85 + // collapsed: whether the pane is a 32px strip (true) or expanded (false) 86 + this.paneVisible = { outline: true, editor: true, preview: true, annotations: false }; 87 + this.paneCollapsed = { outline: true, editor: false, preview: true, annotations: false }; 88 + this.paneWidths = {}; // persisted user widths (px strings), e.g. { outline: '250px' } 71 89 this.paneOrder = ['outline', 'editor', 'preview']; // annotations always far right 72 - this.paneWidths = {}; // user-resized widths, persisted 73 - this.maximizedPane = null; // null or pane name 74 90 75 91 // Scroll sync state 76 92 this._scrollSyncEnabled = true; ··· 83 99 84 100 _loadPrefs() { 85 101 const prefs = loadLayoutPrefs(); 86 - if (prefs.paneVisibility) { 87 - this.paneVisibility = { ...this.paneVisibility, ...prefs.paneVisibility }; 102 + if (prefs.paneVisible) { 103 + this.paneVisible = { ...this.paneVisible, ...prefs.paneVisible }; 88 104 } 105 + // Always keep editor visible 106 + this.paneVisible.editor = true; 89 107 if (prefs.paneCollapsed) { 90 108 this.paneCollapsed = { ...this.paneCollapsed, ...prefs.paneCollapsed }; 91 109 } 110 + // Editor is never collapsed 111 + this.paneCollapsed.editor = false; 112 + if (prefs.paneWidths) { 113 + this.paneWidths = { ...prefs.paneWidths }; 114 + } 92 115 if (Array.isArray(prefs.paneOrder) && prefs.paneOrder.length === 3) { 93 116 this.paneOrder = prefs.paneOrder; 94 117 } 95 - if (prefs.paneWidths) { 96 - this.paneWidths = prefs.paneWidths; 97 - } 98 118 } 99 119 100 120 _savePrefs() { 101 121 saveLayoutPrefs({ 102 - paneVisibility: this.paneVisibility, 122 + paneVisible: this.paneVisible, 103 123 paneCollapsed: this.paneCollapsed, 124 + paneWidths: this.paneWidths, 104 125 paneOrder: this.paneOrder, 105 - paneWidths: this.paneWidths, 106 126 }); 107 127 } 108 128 ··· 154 174 this.previewToggleBtn = this._createToggleBtn('Preview', 'Toggle preview (Cmd+Shift+P)', () => this.togglePane('preview')); 155 175 sidebarToggles.appendChild(this.previewToggleBtn); 156 176 157 - this.annotationsToggleBtn = this._createToggleBtn('Notes', 'Toggle notes (Cmd+Shift+H)', () => this.togglePane('annotations')); 177 + this.annotationsToggleBtn = this._createToggleBtn('Notes', 'Toggle annotations (Cmd+Shift+H)', () => this.togglePane('annotations')); 158 178 sidebarToggles.appendChild(this.annotationsToggleBtn); 159 179 160 180 // Pane order cycle button ··· 198 218 onAnnotationClick: (hl) => this._jumpToHighlight(hl), 199 219 onAnnotationDelete: (id) => this._removeHighlight(id), 200 220 onAnnotationEdit: (id, note) => this._updateHighlightNote(id, note), 201 - onAddNote: () => this._addHighlightFromSelection(), 202 221 onToggle: () => this.togglePane('annotations'), 203 222 }); 204 223 ··· 246 265 this.cmdSuggestionsCleanup = observeCommandBar(this.cmContainer); 247 266 } 248 267 249 - // Apply visibility state 250 - this._applyVisibility(); 268 + // Apply pane state (visibility, collapsed, widths) 269 + this._applyPaneState(); 251 270 252 271 // Initial update 253 272 this.lastContent = this.initialContent; ··· 345 364 } 346 365 347 366 _startResize(resizer, startEvent, indicator) { 348 - const leftPaneName = resizer.dataset.leftPane; 349 - const rightPaneName = resizer.dataset.rightPane; 350 - 351 - // For annotations resizer, left is paneContainer, right is annotations 352 367 const isAnnotationsResizer = resizer.dataset.resizerKey === 'annotations'; 353 368 354 - if (isAnnotationsResizer) { 355 - const rightEl = this.annotationsPane.getElement(); 356 - if (!rightEl) return; 357 - 358 - resizer.classList.add('dragging'); 359 - const startX = startEvent.clientX; 360 - const startWidth = rightEl.offsetWidth; 361 - 362 - document.body.style.userSelect = 'none'; 363 - document.body.style.cursor = 'col-resize'; 364 - 365 - const onMouseMove = (e) => { 366 - const newWidth = Math.max(100, Math.min(600, startWidth - (e.clientX - startX))); 367 - rightEl.style.width = `${newWidth}px`; 368 - rightEl.style.minWidth = `${newWidth}px`; 369 - }; 369 + // Determine which non-editor pane we're resizing. 370 + // For annotations resizer: resize the annotations pane. 371 + // For inner resizers: resize whichever side is NOT the editor. 372 + let targetPaneName; 373 + let targetEl; 370 374 371 - const onMouseUp = () => { 372 - resizer.classList.remove('dragging'); 373 - indicator.classList.remove('visible'); 374 - document.body.style.userSelect = ''; 375 - document.body.style.cursor = ''; 376 - document.removeEventListener('mousemove', onMouseMove); 377 - document.removeEventListener('mouseup', onMouseUp); 378 - }; 379 - 380 - document.addEventListener('mousemove', onMouseMove); 381 - document.addEventListener('mouseup', onMouseUp); 382 - return; 375 + if (isAnnotationsResizer) { 376 + targetPaneName = 'annotations'; 377 + targetEl = this._getPaneElement('annotations'); 378 + } else { 379 + const leftPaneName = resizer.dataset.leftPane; 380 + const rightPaneName = resizer.dataset.rightPane; 381 + // The non-editor side is the target 382 + if (leftPaneName !== 'editor') { 383 + targetPaneName = leftPaneName; 384 + } else { 385 + targetPaneName = rightPaneName; 386 + } 387 + targetEl = this._getPaneElement(targetPaneName); 383 388 } 384 389 385 - // For pane resizers: only resize the non-flex pane, let flex:1 editor absorb the rest 386 - const leftEl = this._getPaneElement(leftPaneName); 387 - const rightEl = this._getPaneElement(rightPaneName); 388 - if (!leftEl || !rightEl) return; 389 - 390 - const leftIsEditor = leftPaneName === 'editor'; 391 - const targetEl = leftIsEditor ? rightEl : leftEl; 392 - const invertDelta = leftIsEditor; 393 - const targetPaneName = leftIsEditor ? rightPaneName : leftPaneName; 390 + if (!targetEl) return; 394 391 395 392 resizer.classList.add('dragging'); 396 393 const startX = startEvent.clientX; 397 394 const startWidth = targetEl.offsetWidth; 398 395 396 + // Determine resize direction: dragging right grows the pane if it's to the left of the resizer 397 + const growsRight = isAnnotationsResizer 398 + ? false // annotations is to the right, dragging right shrinks it 399 + : (resizer.dataset.leftPane === targetPaneName); // left pane grows when dragging right 400 + 399 401 document.body.style.userSelect = 'none'; 400 402 document.body.style.cursor = 'col-resize'; 401 403 404 + let lastWidth = startWidth; 405 + 402 406 const onMouseMove = (e) => { 403 407 const delta = e.clientX - startX; 404 - const newWidth = Math.max(80, startWidth + (invertDelta ? -delta : delta)); 408 + const newWidth = Math.max(100, Math.min(600, startWidth + (growsRight ? delta : -delta))); 405 409 targetEl.style.width = `${newWidth}px`; 406 410 targetEl.style.minWidth = `${newWidth}px`; 411 + targetEl.style.flex = 'none'; 412 + lastWidth = newWidth; 407 413 }; 408 414 409 415 const onMouseUp = () => { ··· 413 419 document.body.style.cursor = ''; 414 420 document.removeEventListener('mousemove', onMouseMove); 415 421 document.removeEventListener('mouseup', onMouseUp); 416 - // Persist the resized width 417 - this.paneWidths[targetPaneName] = targetEl.offsetWidth; 422 + 423 + // Persist the width 424 + this.paneWidths[targetPaneName] = `${lastWidth}px`; 418 425 this._savePrefs(); 419 426 }; 420 427 ··· 423 430 } 424 431 425 432 _handleResizerDoubleClick(resizer) { 426 - const leftPaneName = resizer.dataset.leftPane; 427 - const rightPaneName = resizer.dataset.rightPane; 428 - 429 433 if (resizer.dataset.resizerKey === 'annotations') { 430 - // Toggle annotations between collapsed and expanded 431 434 this.togglePane('annotations'); 432 435 return; 433 436 } 434 437 435 - // If a pane is maximized, restore it 436 - if (this.maximizedPane) { 437 - this._restoreAllPanes(); 438 - this.maximizedPane = null; 439 - return; 440 - } 441 - 442 - // Maximize the left pane (convention: double-click maximizes the pane to the left of the resizer) 443 - this._maximizePane(leftPaneName); 444 - } 445 - 446 - _maximizePane(paneName) { 447 - this.maximizedPane = paneName; 448 - for (const name of this.paneOrder) { 449 - const el = this._getPaneElement(name); 450 - if (!el) continue; 451 - if (name === paneName) { 452 - el.style.flex = '1'; 453 - el.style.display = ''; 454 - el.style.width = ''; 455 - el.style.minWidth = ''; 456 - } else { 457 - el.style.display = 'none'; 458 - } 459 - } 460 - // Hide resizers inside pane container 461 - this.paneContainer.querySelectorAll('.resizer').forEach(r => r.style.display = 'none'); 462 - } 463 - 464 - _restoreAllPanes() { 465 - for (const name of this.paneOrder) { 466 - const el = this._getPaneElement(name); 467 - if (!el) continue; 468 - el.style.flex = ''; 469 - el.style.display = this.paneVisibility[name] ? '' : 'none'; 470 - el.style.width = ''; 471 - el.style.minWidth = ''; 472 - } 473 - this._applyVisibility(); 438 + // Double-click toggles the non-editor pane adjacent to the resizer 439 + const leftPaneName = resizer.dataset.leftPane; 440 + const rightPaneName = resizer.dataset.rightPane; 441 + const target = leftPaneName !== 'editor' ? leftPaneName : rightPaneName; 442 + this.togglePane(target); 474 443 } 475 444 476 445 _getPaneElement(name) { ··· 484 453 } 485 454 486 455 /** 487 - * Toggle a pane. For outline/preview, toggles between collapsed and expanded. 488 - * For annotations, toggles visibility entirely. 456 + * Toggle a pane's collapsed state. 457 + * For non-visible panes (e.g. annotations initially hidden), first show them expanded. 458 + * Toolbar cycle: expanded -> collapsed -> expanded (never hides). 489 459 */ 490 460 togglePane(paneName) { 491 - if (paneName === 'annotations') { 492 - this.paneVisibility.annotations = !this.paneVisibility.annotations; 493 - this._applyVisibility(); 494 - this._savePrefs(); 495 - return; 496 - } 461 + if (paneName === 'editor') return; // Editor cannot be toggled 497 462 498 - if (paneName === 'outline' || paneName === 'preview') { 499 - this.paneCollapsed[paneName] = !this.paneCollapsed[paneName]; 463 + if (!this.paneVisible[paneName]) { 464 + // Not visible yet — show it expanded 465 + this.paneVisible[paneName] = true; 466 + this.paneCollapsed[paneName] = false; 500 467 } else { 501 - this.paneVisibility[paneName] = !this.paneVisibility[paneName]; 468 + // Already visible — toggle collapsed 469 + this.paneCollapsed[paneName] = !this.paneCollapsed[paneName]; 502 470 } 503 - this._applyVisibility(); 471 + 472 + this._applyPaneState(); 504 473 this._savePrefs(); 505 474 } 506 475 507 476 /** 508 - * Apply current visibility and collapsed state to DOM. 477 + * Programmatically hide a pane (display:none). Used by focus mode. 509 478 */ 510 - _applyVisibility() { 511 - if (this.maximizedPane) return; 479 + hidePane(paneName) { 480 + this.paneVisible[paneName] = false; 481 + this._applyPaneState(); 482 + } 512 483 513 - for (const name of this.paneOrder) { 484 + /** 485 + * Programmatically show a pane (restores previous collapsed state). 486 + */ 487 + showPane(paneName) { 488 + this.paneVisible[paneName] = true; 489 + this._applyPaneState(); 490 + } 491 + 492 + /** 493 + * Apply the full pane state to DOM. This is the SINGLE source of truth. 494 + * Reads paneVisible, paneCollapsed, paneWidths and updates: 495 + * - display (visible vs hidden) 496 + * - CSS .collapsed class 497 + * - inline width/minWidth (cleared when collapsed, set when expanded with user width) 498 + * - sidebar component visual state (toggle button icon) 499 + * - resizer visibility 500 + * - toolbar button active states 501 + */ 502 + _applyPaneState() { 503 + const allPanes = ['outline', 'editor', 'preview', 'annotations']; 504 + 505 + for (const name of allPanes) { 514 506 const el = this._getPaneElement(name); 515 507 if (!el) continue; 516 508 517 - if (!this.paneVisibility[name]) { 509 + const visible = this.paneVisible[name]; 510 + const collapsed = this.paneCollapsed[name]; 511 + 512 + if (!visible) { 513 + // Hidden — display:none, clear all inline styles 518 514 el.style.display = 'none'; 519 - continue; 515 + el.style.width = ''; 516 + el.style.minWidth = ''; 517 + el.style.flex = ''; 518 + el.classList.remove('collapsed'); 519 + } else if (collapsed) { 520 + // Visible + collapsed — 32px strip 521 + el.style.display = ''; 522 + el.style.width = '32px'; 523 + el.style.minWidth = '32px'; 524 + el.style.flex = 'none'; 525 + el.classList.add('collapsed'); 526 + } else if (name === 'editor') { 527 + // Editor is always flex:1 528 + el.style.display = ''; 529 + el.style.width = ''; 530 + el.style.minWidth = ''; 531 + el.style.flex = '1'; 532 + el.classList.remove('collapsed'); 533 + } else { 534 + // Visible + expanded — use persisted width or default 535 + el.style.display = ''; 536 + const width = this.paneWidths[name] || DEFAULT_WIDTHS[name] || ''; 537 + el.style.width = width; 538 + el.style.minWidth = width; 539 + el.style.flex = 'none'; 540 + el.classList.remove('collapsed'); 520 541 } 521 542 522 - el.style.display = ''; 523 - 524 - if (name === 'outline') { 525 - const collapsed = this.paneCollapsed.outline; 526 - this.outlineSidebar.setCollapsed(collapsed); 527 - if (!collapsed) { 528 - if (this.paneWidths.outline) { 529 - el.style.width = `${this.paneWidths.outline}px`; 530 - el.style.minWidth = '80px'; 531 - } else { 532 - el.style.width = ''; 533 - el.style.minWidth = ''; 534 - } 535 - } else { 536 - el.style.width = ''; 537 - el.style.minWidth = ''; 538 - } 539 - } else if (name === 'preview') { 540 - const collapsed = this.paneCollapsed.preview; 541 - this.previewSidebar.setCollapsed(collapsed); 542 - if (!collapsed) { 543 - if (this.paneWidths.preview) { 544 - el.style.width = `${this.paneWidths.preview}px`; 545 - el.style.minWidth = '80px'; 546 - } else { 547 - el.style.width = ''; 548 - el.style.minWidth = ''; 549 - } 550 - } else { 551 - el.style.width = ''; 552 - el.style.minWidth = ''; 553 - } 554 - } 543 + // Update sidebar component visual state 544 + this._updateSidebarVisual(name, collapsed && visible); 555 545 } 556 546 557 - // Annotations pane 558 - if (this.annotationsPane) { 559 - this.annotationsPane.getElement().style.display = this.paneVisibility.annotations ? '' : 'none'; 560 - this.annotationsResizer.style.display = this.paneVisibility.annotations ? '' : 'none'; 547 + // Annotations resizer visibility: shown only when annotations is visible AND expanded 548 + if (this.annotationsResizer) { 549 + const annVisible = this.paneVisible.annotations; 550 + const annCollapsed = this.paneCollapsed.annotations; 551 + this.annotationsResizer.style.display = (annVisible && !annCollapsed) ? '' : 'none'; 561 552 } 562 553 563 - // Hide resizers next to collapsed or hidden panes 554 + // Inner resizers: shown only when both adjacent panes are visible AND at least one is expanded 564 555 this.paneContainer.querySelectorAll('.resizer').forEach(r => { 565 556 const left = r.dataset.leftPane; 566 557 const right = r.dataset.rightPane; 567 - const leftUsable = this.paneVisibility[left] && !this.paneCollapsed[left]; 568 - const rightUsable = this.paneVisibility[right] && !this.paneCollapsed[right]; 569 - r.style.display = (leftUsable && rightUsable) ? '' : 'none'; 558 + const leftOk = this.paneVisible[left] && !this.paneCollapsed[left]; 559 + const rightOk = this.paneVisible[right] && !this.paneCollapsed[right]; 560 + // Show resizer if both panes are visible and at least one is expanded 561 + const bothVisible = this.paneVisible[left] && this.paneVisible[right]; 562 + r.style.display = (bothVisible && (leftOk || rightOk)) ? '' : 'none'; 570 563 }); 571 564 572 - // Update toggle button states — active means expanded (not collapsed) 573 - this.outlineToggleBtn.classList.toggle('active', this.paneVisibility.outline && !this.paneCollapsed.outline); 574 - this.editorToggleBtn.classList.toggle('active', this.paneVisibility.editor); 575 - this.previewToggleBtn.classList.toggle('active', this.paneVisibility.preview && !this.paneCollapsed.preview); 576 - this.annotationsToggleBtn.classList.toggle('active', this.paneVisibility.annotations); 565 + // Update toolbar button states — "active" means visible+expanded 566 + this.outlineToggleBtn.classList.toggle('active', this.paneVisible.outline && !this.paneCollapsed.outline); 567 + this.editorToggleBtn.classList.toggle('active', this.paneVisible.editor); 568 + this.previewToggleBtn.classList.toggle('active', this.paneVisible.preview && !this.paneCollapsed.preview); 569 + this.annotationsToggleBtn.classList.toggle('active', this.paneVisible.annotations && !this.paneCollapsed.annotations); 570 + } 571 + 572 + /** 573 + * Update a sidebar component's visual collapsed state (CSS class + toggle button icon). 574 + * The layout owns the state; sidebars only reflect it visually. 575 + */ 576 + _updateSidebarVisual(paneName, isCollapsed) { 577 + switch (paneName) { 578 + case 'outline': 579 + if (this.outlineSidebar) this.outlineSidebar.setCollapsed(isCollapsed); 580 + break; 581 + case 'preview': 582 + if (this.previewSidebar) this.previewSidebar.setCollapsed(isCollapsed); 583 + break; 584 + case 'annotations': 585 + if (this.annotationsPane) this.annotationsPane.setCollapsed(isCollapsed); 586 + break; 587 + } 588 + } 589 + 590 + // Keep old name as alias for any external callers 591 + _applyVisibility() { 592 + this._applyPaneState(); 577 593 } 578 594 579 595 /** ··· 631 647 */ 632 648 _syncPreviewScroll() { 633 649 if (!this.cmEditor || !this.previewSidebar) return; 634 - if (!this.paneVisibility.preview || this.paneCollapsed.preview) return; 650 + if (!this.paneVisible.preview || this.paneCollapsed.preview) return; 635 651 636 652 const scroller = this.cmContainer?.querySelector('.cm-scroller'); 637 653 if (!scroller) return; ··· 653 669 */ 654 670 _syncPreviewToHeading(headerText) { 655 671 if (!this.previewSidebar) return; 656 - if (!this.paneVisibility.preview || this.paneCollapsed.preview) return; 672 + if (!this.paneVisible.preview || this.paneCollapsed.preview) return; 657 673 658 674 const previewContent = this.previewSidebar.getContentElement(); 659 675 if (!previewContent) return; ··· 680 696 e.preventDefault(); 681 697 this.togglePane('preview'); 682 698 } 683 - // Cmd+Shift+H: Toggle notes pane 699 + // Cmd+Shift+H: Toggle annotations 684 700 if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'h') { 685 701 e.preventDefault(); 686 702 this.togglePane('annotations'); 687 703 } 688 - // Cmd+Shift+L: Add note at selection/cursor 689 - if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'l') { 704 + // Cmd+H: Add highlight on selected text 705 + if ((e.metaKey || e.ctrlKey) && e.key === 'h' && !e.shiftKey) { 690 706 e.preventDefault(); 691 707 this._addHighlightFromSelection(); 692 708 } ··· 704 720 705 721 const state = this.cmEditor.state; 706 722 const sel = state.selection.main; 723 + if (sel.from === sel.to) return; // No selection 707 724 708 - // If no selection, anchor to the current line 709 - let from = sel.from; 710 - let to = sel.to; 711 - if (from === to) { 712 - const line = state.doc.lineAt(from); 713 - from = line.from; 714 - to = line.to; 715 - } 716 - 725 + const text = state.doc.sliceString(sel.from, sel.to); 717 726 const highlight = { 718 727 id: generateHighlightId(), 719 - from, 720 - to, 721 - text: '', // notes don't store selected text 728 + from: sel.from, 729 + to: sel.to, 730 + text, 722 731 note: '', 723 732 color: nextHighlightColor(), 724 733 }; ··· 727 736 this._syncAnnotationsPane(); 728 737 this._persistHighlights(); 729 738 730 - // Auto-show notes pane 731 - if (!this.paneVisibility.annotations) { 732 - this.togglePane('annotations'); 739 + // Auto-show annotations pane expanded 740 + if (!this.paneVisible.annotations || this.paneCollapsed.annotations) { 741 + this.paneVisible.annotations = true; 742 + this.paneCollapsed.annotations = false; 743 + this._applyPaneState(); 744 + this._savePrefs(); 733 745 } 734 746 } 735 747 ··· 759 771 _syncAnnotationsPane() { 760 772 if (!this.cmEditor || !this.annotationsPane) return; 761 773 const highlights = getHighlights(this.cmEditor.state); 762 - this.annotationsPane.update(highlights, this.cmEditor.state.doc); 774 + this.annotationsPane.update(highlights); 763 775 } 764 776 765 777 _persistHighlights() { ··· 955 967 if (this.isFocusMode) return; 956 968 957 969 this.isFocusMode = true; 958 - this.originalContainerStyles = this.container.style.cssText; 970 + // Save current state so we can restore on exit 971 + this.preFocusState = { 972 + paneVisible: { ...this.paneVisible }, 973 + paneCollapsed: { ...this.paneCollapsed }, 974 + }; 959 975 this.wrapper.classList.add('focus-mode'); 960 976 this.focusBtn.classList.add('active'); 961 977 962 978 // Hide everything except editor 963 - if (this.outlineSidebar) this.outlineSidebar.getElement().style.display = 'none'; 964 - if (this.previewSidebar) this.previewSidebar.getElement().style.display = 'none'; 965 - if (this.annotationsPane) { 966 - this.annotationsPane.getElement().style.display = 'none'; 967 - this.annotationsResizer.style.display = 'none'; 968 - } 969 - this.paneContainer.querySelectorAll('.resizer').forEach(r => r.style.display = 'none'); 979 + this.paneVisible = { outline: false, editor: true, preview: false, annotations: false }; 980 + this.paneCollapsed = { ...this.paneCollapsed, editor: false }; 981 + this._applyPaneState(); 970 982 971 983 if (this.cmEditor) CodeMirror.focus(this.cmEditor); 972 984 } ··· 975 987 if (!this.isFocusMode) return; 976 988 977 989 this.isFocusMode = false; 978 - this.container.style.cssText = this.originalContainerStyles; 979 990 this.wrapper.classList.remove('focus-mode'); 980 991 this.focusBtn.classList.remove('active'); 981 992 982 - // Restore visibility 983 - if (this.outlineSidebar) this.outlineSidebar.getElement().style.display = ''; 984 - if (this.previewSidebar) this.previewSidebar.getElement().style.display = ''; 985 - if (this.annotationsPane) { 986 - this.annotationsPane.getElement().style.display = ''; 987 - this.annotationsResizer.style.display = ''; 993 + // Restore previous state 994 + if (this.preFocusState) { 995 + this.paneVisible = this.preFocusState.paneVisible; 996 + this.paneCollapsed = this.preFocusState.paneCollapsed; 997 + this.preFocusState = null; 988 998 } 989 - this.paneContainer.querySelectorAll('.resizer').forEach(r => r.style.display = ''); 990 - 991 - this._applyVisibility(); 999 + this._applyPaneState(); 992 1000 993 1001 if (this.cmEditor) CodeMirror.focus(this.cmEditor); 994 1002 }
+19 -71
extensions/editor/home.css
··· 61 61 Sidebar shared styles 62 62 ===================================================================== */ 63 63 64 + /* ── Shared sidebar/pane base styles ── */ 65 + 64 66 .outline-sidebar, 65 - .preview-sidebar { 67 + .preview-sidebar, 68 + .annotations-pane { 66 69 display: flex; 67 70 flex-direction: column; 68 71 background: var(--base00); 69 72 font-family: var(--theme-font-mono, 'SF Mono', 'Fira Code', 'Consolas', monospace); 70 73 font-size: 12px; 71 74 overflow: hidden; 72 - transition: width 0.15s ease, min-width 0.15s ease; 75 + /* No CSS transition on width — layout sets inline styles for instant response */ 73 76 } 74 77 75 78 .outline-sidebar { 76 - width: 20%; 77 - min-width: 80px; 78 79 border-right: 1px solid var(--base02); 79 80 } 80 81 81 82 .preview-sidebar { 82 - width: 40%; 83 - min-width: 80px; 84 83 border-left: 1px solid var(--base02); 85 84 font-family: var(--theme-font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif); 86 85 font-size: 14px; 87 86 } 88 87 89 - /* Collapsed state */ 90 - .outline-sidebar.collapsed, 91 - .preview-sidebar.collapsed { 92 - width: 32px; 93 - min-width: 32px; 94 - } 88 + /* ── Collapsed state (uniform for all panes) ── */ 89 + /* When collapsed, hide content and title, show only toggle button */ 95 90 96 91 .outline-sidebar.collapsed .sidebar-content, 97 92 .preview-sidebar.collapsed .sidebar-content, 98 - .outline-sidebar.collapsed .preview-content, 99 - .preview-sidebar.collapsed .preview-content { 93 + .preview-sidebar.collapsed .preview-content, 94 + .annotations-pane.collapsed .annotations-content { 100 95 display: none; 101 96 } 102 97 103 98 .outline-sidebar.collapsed .sidebar-title, 104 - .preview-sidebar.collapsed .sidebar-title { 99 + .preview-sidebar.collapsed .sidebar-title, 100 + .annotations-pane.collapsed .sidebar-title { 105 101 display: none; 106 102 } 107 103 ··· 109 105 display: none; 110 106 } 111 107 108 + .annotations-pane.collapsed .annotations-count { 109 + display: none; 110 + } 111 + 112 112 .outline-sidebar.collapsed .sidebar-header, 113 - .preview-sidebar.collapsed .sidebar-header { 113 + .preview-sidebar.collapsed .sidebar-header, 114 + .annotations-pane.collapsed .sidebar-header { 114 115 justify-content: center; 115 116 padding: 8px 4px; 116 117 } ··· 374 375 Annotations Pane (far right) 375 376 ===================================================================== */ 376 377 378 + /* annotations-pane inherits shared styles from above */ 379 + 377 380 .annotations-pane { 378 - display: flex; 379 - flex-direction: column; 380 - background: var(--base00); 381 - font-family: var(--theme-font-mono, 'SF Mono', 'Fira Code', 'Consolas', monospace); 382 - font-size: 12px; 383 - overflow: hidden; 384 - width: 260px; 385 - min-width: 260px; 386 381 border-left: 1px solid var(--base02); 387 - transition: width 0.15s ease, min-width 0.15s ease; 388 - } 389 - 390 - .annotations-pane.collapsed { 391 - width: 32px; 392 - min-width: 32px; 393 - } 394 - 395 - .annotations-pane.collapsed .annotations-content { 396 - display: none; 397 - } 398 - 399 - .annotations-pane.collapsed .sidebar-title { 400 - display: none; 401 - } 402 - 403 - .annotations-pane.collapsed .annotations-count { 404 - display: none; 405 - } 406 - 407 - .annotations-pane.collapsed .annotation-add-btn { 408 - display: none; 409 - } 410 - 411 - .annotations-pane.collapsed .sidebar-header { 412 - justify-content: center; 413 - padding: 8px 4px; 414 382 } 415 383 416 384 .annotations-count { ··· 421 389 border-radius: 8px; 422 390 min-width: 18px; 423 391 text-align: center; 424 - } 425 - 426 - .annotation-add-btn { 427 - background: none; 428 - border: 1px solid var(--base02); 429 - color: var(--base04); 430 - cursor: pointer; 431 - padding: 0 6px; 432 - font-size: 14px; 433 - font-weight: 600; 434 - line-height: 1; 435 - border-radius: 3px; 436 - transition: all 0.1s; 437 - flex-shrink: 0; 438 - margin-left: auto; 439 - } 440 - 441 - .annotation-add-btn:hover { 442 - background: var(--base02); 443 - color: var(--base05); 444 392 } 445 393 446 394 .annotations-content {
+16 -16
extensions/editor/outline-sidebar.js
··· 74 74 this.container = options.container; 75 75 this.onHeaderClick = options.onHeaderClick; 76 76 this.onFoldToggle = options.onFoldToggle; // callback: (header, shouldFold) => void 77 - this.onToggle = options.onToggle; // callback: () => void — called when internal toggle clicked 77 + this.onToggle = options.onToggle; // callback: () => void — layout handles state 78 78 this.collapsed = false; 79 79 this.headers = []; 80 80 this.parentSet = new Set(); ··· 129 129 this.toggleBtn.innerHTML = '\u25C0'; // left arrow 130 130 this.toggleBtn.tabIndex = -1; 131 131 this.toggleBtn.addEventListener('mousedown', (e) => e.preventDefault()); 132 - this.toggleBtn.addEventListener('click', () => { 133 - if (this.onToggle) { 134 - this.onToggle(); 135 - } else { 136 - this.toggle(); 137 - } 138 - }); 132 + this.toggleBtn.addEventListener('click', () => this.toggle()); 139 133 this.header.appendChild(this.toggleBtn); 140 134 141 135 this.element.appendChild(this.header); ··· 398 392 } 399 393 400 394 /** 401 - * Toggle sidebar collapsed state. 395 + * Set collapsed visual state. Called by layout — sidebar does NOT own this state. 396 + * Only updates CSS class and toggle button icon. 402 397 */ 403 - toggle() { 404 - this.setCollapsed(!this.collapsed); 405 - } 406 - 407 398 setCollapsed(collapsed) { 408 399 this.collapsed = collapsed; 409 - this.element.classList.toggle('collapsed', this.collapsed); 410 - 411 - if (this.collapsed) { 400 + this.element.classList.toggle('collapsed', collapsed); 401 + if (collapsed) { 412 402 this.toggleBtn.innerHTML = '\u2261'; // ≡ 413 403 this.toggleBtn.title = 'Show Outline'; 414 404 } else { 415 405 this.toggleBtn.innerHTML = '\u25C0'; // ◀ 416 406 this.toggleBtn.title = 'Hide Outline'; 407 + } 408 + } 409 + 410 + /** 411 + * Toggle sidebar collapsed state (for internal toggle button click). 412 + * Delegates to onToggle callback which should call layout.togglePane(). 413 + */ 414 + toggle() { 415 + if (this.onToggle) { 416 + this.onToggle(); 417 417 } 418 418 } 419 419
+14 -16
extensions/editor/preview-sidebar.js
··· 106 106 export class PreviewSidebar { 107 107 constructor(options) { 108 108 this.container = options.container; 109 - this.onToggle = options.onToggle; // callback: () => void — called when internal toggle clicked 109 + this.onToggle = options.onToggle; // callback: () => void — layout handles state 110 110 this.collapsed = false; 111 111 112 112 // Create sidebar element ··· 127 127 this.toggleBtn.innerHTML = '\u25B6'; // ▶ 128 128 this.toggleBtn.tabIndex = -1; 129 129 this.toggleBtn.addEventListener('mousedown', (e) => e.preventDefault()); 130 - this.toggleBtn.addEventListener('click', () => { 131 - if (this.onToggle) { 132 - this.onToggle(); 133 - } else { 134 - this.toggle(); 135 - } 136 - }); 130 + this.toggleBtn.addEventListener('click', () => this.toggle()); 137 131 this.header.appendChild(this.toggleBtn); 138 132 139 133 this.element.appendChild(this.header); ··· 155 149 } 156 150 157 151 /** 158 - * Toggle sidebar collapsed state. 152 + * Set collapsed visual state. Called by layout — sidebar does NOT own this state. 159 153 */ 160 - toggle() { 161 - this.setCollapsed(!this.collapsed); 162 - } 163 - 164 154 setCollapsed(collapsed) { 165 155 this.collapsed = collapsed; 166 - this.element.classList.toggle('collapsed', this.collapsed); 167 - 168 - if (this.collapsed) { 156 + this.element.classList.toggle('collapsed', collapsed); 157 + if (collapsed) { 169 158 this.toggleBtn.innerHTML = '\u25CE'; // ◎ 170 159 this.toggleBtn.title = 'Show Preview'; 171 160 } else { 172 161 this.toggleBtn.innerHTML = '\u25B6'; // ▶ 173 162 this.toggleBtn.title = 'Hide Preview'; 163 + } 164 + } 165 + 166 + /** 167 + * Toggle sidebar collapsed state (for internal toggle button click). 168 + */ 169 + toggle() { 170 + if (this.onToggle) { 171 + this.onToggle(); 174 172 } 175 173 } 176 174