Changeset 0.18.1 (#80)
This commit is contained in:
244
docs/DESIGN-0.18.1.md
Normal file
244
docs/DESIGN-0.18.1.md
Normal 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 295–365):**
|
||||
- `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 642–737):**
|
||||
- `.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
|
||||
@@ -68,7 +68,7 @@ v0.17.3 Notes Graph + Wikilinks ✅
|
||||
│
|
||||
v0.18.0 Memory ✅ (user + persona scopes, review pipeline)
|
||||
│
|
||||
v0.18.1 Side Panel Architecture (independent panels, dual-view)
|
||||
v0.18.1 Side Panel Architecture ✅ (single-slot, ctx.ui primitives, mermaid refactor)
|
||||
│
|
||||
v0.19.0 Projects / Workspaces + Notifications
|
||||
│
|
||||
@@ -454,34 +454,45 @@ See [DESIGN-0.18.0.md](DESIGN-0.18.0.md) for full spec.
|
||||
|
||||
---
|
||||
|
||||
## v0.18.1 — Side Panel Architecture
|
||||
## v0.18.1 — Side Panel Architecture ✅
|
||||
|
||||
Refactor the side panel system so Notes, HTML/code previews, and future
|
||||
panels (knowledge base browser, search results) operate independently
|
||||
rather than sharing a single slot. Currently, opening Notes hides the
|
||||
preview panel and vice versa — this is confusing when reviewing AI-
|
||||
generated HTML/documents while also managing notes.
|
||||
Refactored the side panel from a shared-tab layout to independent
|
||||
single-slot panels. Any action fills the slot, replacing whatever was
|
||||
there — no tabs, no association between panel types. Extension UI
|
||||
primitives give browser extensions safe access to host app features.
|
||||
|
||||
Depends on: Notes graph + wikilinks (v0.17.3). Prerequisite for: live
|
||||
collaboration (v0.23.0) where multiple panels need simultaneous visibility.
|
||||
|
||||
**Panel System**
|
||||
- [ ] Panel registry: named panels with independent open/close state
|
||||
- [ ] Dual-view mode: two panels side-by-side (configurable split ratio)
|
||||
- [ ] Tab-persistent state: each panel remembers scroll position, open note, etc.
|
||||
- [ ] Panel priority: when space is tight (mobile), most-recently-opened wins
|
||||
- [ ] Keyboard shortcut: Ctrl/Cmd+\ cycles panels, Ctrl/Cmd+Shift+\ toggles dual
|
||||
- [x] Panel registry: named panels with independent open/close state
|
||||
- [x] Single-slot model: action-driven, each open replaces the previous
|
||||
- [x] Dual-view mode: two panels side-by-side (configurable split ratio via drag divider)
|
||||
- [x] Panel-persistent state: each panel remembers scroll position, open note, etc.
|
||||
- [x] Header label shows active panel name (replaces tab bar)
|
||||
- [x] Keyboard shortcut: Ctrl/Cmd+\ cycles panels, Ctrl/Cmd+Shift+\ toggles dual
|
||||
|
||||
**Preview Panel**
|
||||
- [ ] Dedicated preview panel (independent of Notes)
|
||||
- [ ] HTML/document preview with iframe sandbox
|
||||
- [ ] Code output preview (extension-rendered blocks)
|
||||
- [ ] Live-updating during streaming responses
|
||||
- [x] Dedicated preview panel (independent of Notes)
|
||||
- [x] HTML/document preview with iframe sandbox
|
||||
- [x] Code output preview (extension-rendered blocks via pop-out)
|
||||
- [x] Live-updating during streaming responses (500ms debounce)
|
||||
|
||||
**Extension UI Primitives**
|
||||
- [x] `ctx.ui.toast()` — toast notifications
|
||||
- [x] `ctx.ui.openPreview()` — load HTML into side panel preview
|
||||
- [x] `ctx.ui.isDark()` — theme detection
|
||||
- [x] `ctx.ui.isMobile()` — viewport width check
|
||||
- [x] `ctx.ui.isPanelOpen()` — side panel visibility
|
||||
- [x] `ctx.ui.confirm()` — modal confirm dialog
|
||||
- [x] Mermaid extension refactored to use primitives exclusively (zero direct globals)
|
||||
- [x] Context-aware expand: single button, fullscreen or side panel pop-out
|
||||
|
||||
**Mobile**
|
||||
- [ ] Swipe navigation between panels
|
||||
- [ ] Bottom sheet mode for secondary panel
|
||||
- [ ] Graceful collapse to single-panel on narrow viewports
|
||||
- [x] Swipe navigation between panels
|
||||
- [x] Tap-to-close overlay
|
||||
- [x] Graceful collapse to single-panel on narrow viewports
|
||||
- [x] Enlarged touch targets, fullscreen close button (40px desktop, 48px mobile)
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user