Changeset 0.18.1 (#80)

This commit is contained in:
2026-02-28 19:41:59 +00:00
parent e4a943b03e
commit a591b810a9
13 changed files with 1415 additions and 197 deletions

244
docs/DESIGN-0.18.1.md Normal file
View File

@@ -0,0 +1,244 @@
# 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