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

245 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
```js
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:**
```js
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:
```html
<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:
```js
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