This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/docs/DESIGN-0.18.1.md
2026-02-28 19:41:59 +00:00

9.4 KiB
Raw Blame History

Design — v0.18.1 Side Panel Architecture

Problem

The side panel is a single <aside> with two mutually exclusive tabs (Preview and Notes) toggled by display: none. Opening Notes kills the preview and vice versa. As more panel content arrives (KB browser, search results, memory viewer), the single-slot model doesn't scale. The current implementation also loses per-panel state (scroll position, active note, preview content) on tab switches.

This is also prerequisite plumbing for v0.23.0 multi-participant channels, where multiple panels need simultaneous visibility.

Current State

HTML: Single <aside id="sidePanel"> containing:

  • Resize handle (sidePanelResize)
  • Header with tab bar (Preview | Notes) + action buttons (clear, fullscreen, close)
  • Body with two .side-panel-page divs toggled by display

JS (ui-format.js lines 295365):

  • openSidePanel(tab) — adds .open class, calls switchSidePanelTab
  • closeSidePanel() — removes .open and .fullscreen, clears inline width
  • switchSidePanelTab(tab) — toggles active class on tab buttons, toggles display on page divs
  • _initSidePanelResize() — mousedown/move/up drag handler for width resize

Callers (4 total):

  1. toggleHTMLPreview()openSidePanel('preview') (ui-format.js)
  2. openNotes()openSidePanel('notes') / closeSidePanel() (notes.js)
  3. Escape handler → closeSidePanel() (app.js)
  4. Inline onclick handlers in index.html (tab buttons, close, fullscreen, clear)

CSS (styles.css lines 642737):

  • .side-panel base: width: 0; min-width: 0; overflow: hidden, transition on width
  • .side-panel.open: width: 480px; min-width: 480px
  • .side-panel.fullscreen: fixed positioning, 100% width
  • Mobile (@media max-width: 768px): open = fixed overlay at 100vw

Design

Panel Registry

A lightweight registry in a new panels.js file. Each panel is a named entry with its own state and DOM container. The registry owns all open/close/focus logic — individual panels never touch the <aside> directly.

const PanelRegistry = {
    _panels: {},     // { name: { element, onOpen, onClose, onFocus, state } }
    _active: null,   // currently visible panel name (single-view mode)
    _secondary: null, // second visible panel (dual-view mode)
    _dualMode: false,

    register(name, opts) { ... },
    open(name) { ... },
    close(name) { ... },
    toggle(name) { ... },
    closeAll() { ... },
    isOpen(name) { ... },
    active() { ... },
};

Panel registration contract:

PanelRegistry.register('notes', {
    element: document.getElementById('sidePanelNotes'),
    onOpen()  { loadNotesList(); loadNoteFolders(); },
    onClose() { /* optional cleanup */ },
    onFocus() { /* optional re-activate */ },
    state: {},  // panel-specific state bag (scroll pos, etc.)
});

Panels register themselves during initialization — Notes registers in notes.js, Preview registers in ui-format.js. This keeps domain logic co-located with its panel.

DOM Structure

Replace the hardcoded tab bar with a registry-driven header. The <aside> structure stays identical in shape (no layout break), but tabs render dynamically from registered panels:

<aside class="side-panel" id="sidePanel">
    <div class="side-panel-resize" id="sidePanelResize"></div>
    <div class="side-panel-header">
        <div class="side-panel-tabs" id="sidePanelTabs">
            <!-- rendered by PanelRegistry from registered panels -->
        </div>
        <div class="side-panel-actions" id="sidePanelActions">
            <!-- per-panel action buttons injected by active panel -->
            <button class="side-panel-action-btn" id="sidePanelFullscreenBtn" ...>
            <button class="side-panel-close" ...>
        </div>
    </div>
    <div class="side-panel-body" id="sidePanelBody">
        <!-- all .side-panel-page divs, shown/hidden by registry -->
    </div>
</aside>

The tab bar is re-rendered on register() and on open()/close(). Each panel can declare actions — an array of button configs that appear in the header when that panel is active (replacing the current hardcoded clear button for preview).

State Preservation

Each panel gets a state bag on its registry entry. Before hiding a panel, the registry calls a saveState() hook (if provided) which can snapshot scroll position, selection, etc. On re-show, restoreState() re-applies. This is opt-in — panels that don't need it don't implement the hooks.

Notes state: { scrollTop, editingNoteId, viewMode: 'list'|'editor'|'graph' } Preview state: { srcdoc, hasContent }

Dual-View Mode (Phase 2)

When enabled, the panel body splits into two panes with an internal divider. _active shows on the left (or top on mobile), _secondary on the right (or bottom). The outer resize handle still controls total panel width; an inner divider controls the split ratio.

Dual mode activates via:

  • Ctrl/Cmd+Shift+\ keyboard shortcut
  • Dragging a tab to the other half (stretch goal)

Per-Panel Action Buttons

Currently Preview has a "Clear" button and both panels share Fullscreen and Close. The new model:

  • Global actions (always visible): Fullscreen, Close
  • Panel actions (per-panel, in active panel's header slot):
    • Preview: Clear
    • Notes: (none currently, but room for "New Note" shortcut)
    • Future panels declare their own

Panels declare actions at registration time:

PanelRegistry.register('preview', {
    element: document.getElementById('sidePanelPreview'),
    actions: [
        { id: 'sidePanelClearBtn', icon: '...svg...', title: 'Clear preview',
          onClick: clearPreview, visible: () => hasPreviewContent() },
    ],
});

Preview Enhancements (Phase 3)

  • Live-update during streaming: The chat streaming handler already accumulates HTML code blocks. On each chunk that completes a fenced code block tagged html, if the preview panel is open, update the iframe's srcdoc. Debounce at 500ms to avoid thrashing.
  • Extension block preview: Mermaid, KaTeX, etc. render inline in chat. Add a "Pop out" button on extension-rendered blocks that opens the rendered output in the preview panel (full-size, zoomable).

Mobile (Phase 4)

  • Swipe navigation: Touch event handlers on .side-panel-body — swipe left/right cycles through open panels. Uses CSS transform: translateX() with a 150px threshold for commit.
  • Bottom sheet mode: On viewports < 768px, the secondary panel in dual mode renders as a bottom sheet (40% height) instead of side-by- side.
  • Priority collapse: When space is tight and dual mode can't fit (< 600px combined), collapse to single-panel with most-recently- opened winning. The displaced panel becomes accessible via tab bar.

Keyboard Shortcuts

Shortcut Action
Ctrl/Cmd+\ Cycle active panel (or open panel container if closed)
Ctrl/Cmd+Shift+\ Toggle dual-view mode
Escape Close panel container (existing behavior, unchanged)

Phases

Phase 1 — Panel Registry + State Management

Extract panel logic from ui-format.js into panels.js. Implement PanelRegistry with register/open/close/toggle/closeAll. Migrate Notes and Preview to register as panels. Dynamic tab bar rendering. Per-panel action buttons. State preservation hooks.

Files changed:

  • New: src/js/panels.js
  • Modified: src/js/ui-format.js (remove panel functions, add preview registration)
  • Modified: src/js/notes.js (replace openSidePanel/closeSidePanel calls with PanelRegistry.open/close)
  • Modified: src/js/app.js (update Escape handler, add keyboard shortcuts, replace _initSidePanelResize call)
  • Modified: src/index.html (remove hardcoded tab buttons, add panels.js script tag, make tab bar dynamic)
  • No CSS changes (reuse existing .side-panel-tab styles)

Backward compatibility: All existing panel behavior works identically. openSidePanel() and closeSidePanel() become thin wrappers around PanelRegistry during transition, then get removed after all callers migrate.

Phase 2 — Dual-View Layout

CSS grid split inside .side-panel-body. Internal divider with drag resize. Keyboard shortcut for dual toggle. Tab drag to split (stretch).

Files changed:

  • Modified: src/js/panels.js (dual-view logic)
  • Modified: src/css/styles.css (grid layout for dual panes)

Phase 3 — Preview Enhancements

Live-update preview during streaming. Extension block "pop out" button.

Files changed:

  • Modified: src/js/ui-format.js (pop-out button on extension blocks)
  • Modified: src/js/chat.js (streaming hook for live preview update)
  • Modified: src/js/panels.js (debounced preview update API)

Phase 4 — Mobile

Swipe navigation, bottom sheet mode, priority collapse.

Files changed:

  • Modified: src/js/panels.js (touch handlers, priority logic)
  • Modified: src/css/styles.css (bottom sheet layout, responsive breakpoints)

Migration Checklist

  • openSidePanel(tab)PanelRegistry.open(tab)
  • closeSidePanel()PanelRegistry.closeAll()
  • switchSidePanelTab(tab)PanelRegistry.open(tab) (open implies focus)
  • toggleSidePanelFullscreen() → stays global (operates on container, not individual panels)
  • _initSidePanelResize() → moves to panels.js (operates on container)
  • Escape handler: PanelRegistry.closeAll()
  • openNotes() toggle logic: PanelRegistry.toggle('notes')
  • toggleHTMLPreview(): PanelRegistry.open('preview') + update iframe content