feat: miller column browser, offline cache, search, breadcrumbs

- Dynamic miller column sizing with flexbox layout
- Root column (160px), middle slices (48px), active column (flex)
- Auto-scroll browser to rightmost column on navigation
- Offline indicator UI and IndexedDB cache integration
- /api/documents endpoint for client-side document caching
- Breadcrumb navigation in document content area
- FTS5 search with SQLite virtual table
- Client-side Mermaid and KaTeX rendering via CDN
- Deep sample content (advanced-topics/raft-deep-dive)
- Border removal (only search button keeps radius)
- Full viewport layout with proper borders between sidebar/content
This commit is contained in:
2026-04-30 11:55:03 -04:00
parent e45eeeb600
commit 780ff3a02c
40 changed files with 2986 additions and 190 deletions

View File

@@ -0,0 +1,101 @@
package app
import (
"context"
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"
"github.com/fsnotify/fsnotify"
)
type FileWatcher struct {
watcher *fsnotify.Watcher
rootDir string
onChange func(path string)
logger *slog.Logger
}
func NewFileWatcher(rootDir string, logger *slog.Logger) (*FileWatcher, error) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, fmt.Errorf("create fsnotify watcher: %w", err)
}
fw := &FileWatcher{
watcher: watcher,
rootDir: rootDir,
logger: logger,
}
if err := fw.addWatches(); err != nil {
watcher.Close()
return nil, err
}
return fw, nil
}
func (fw *FileWatcher) addWatches() error {
return filepath.Walk(fw.rootDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
if info.IsDir() {
if err := fw.watcher.Add(path); err != nil {
fw.logger.Warn("watch directory", "path", path, "error", err)
}
}
return nil
})
}
func (fw *FileWatcher) SetOnChange(fn func(path string)) {
fw.onChange = fn
}
func (fw *FileWatcher) Run(ctx context.Context) {
defer fw.watcher.Close()
for {
select {
case <-ctx.Done():
return
case event, ok := <-fw.watcher.Events:
if !ok {
return
}
if fw.shouldProcess(event) {
if event.Op&fsnotify.Create == fsnotify.Create {
if info, err := os.Stat(event.Name); err == nil && info.IsDir() {
fw.watcher.Add(event.Name)
}
}
if fw.onChange != nil {
fw.onChange(event.Name)
}
}
case err, ok := <-fw.watcher.Errors:
if !ok {
return
}
fw.logger.Warn("fsnotify error", "error", err)
}
}
}
func (fw *FileWatcher) shouldProcess(event fsnotify.Event) bool {
if event.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Remove|fsnotify.Rename) == 0 {
return false
}
name := filepath.Base(event.Name)
if strings.HasPrefix(name, ".") || strings.HasSuffix(name, "~") {
return false
}
if filepath.Ext(event.Name) == ".md" || event.Op&fsnotify.Remove != 0 {
return true
}
return false
}