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

@@ -91,6 +91,7 @@ func New(ctx context.Context, cfg config.Config, logger *slog.Logger) (*App, err
func (a *App) Run(ctx context.Context) error {
go a.watchDocuments(ctx)
go a.syncPoll(ctx)
go func() {
<-ctx.Done()
@@ -107,7 +108,26 @@ func (a *App) Run(ctx context.Context) error {
}
func (a *App) watchDocuments(ctx context.Context) {
ticker := time.NewTicker(2 * time.Second)
watcher, err := NewFileWatcher(a.cfg.Content.SourceDir, a.logger)
if err != nil {
a.logger.Warn("file watcher failed, falling back to polling only", "error", err)
<-ctx.Done()
return
}
watcher.SetOnChange(func(path string) {
if _, err := a.docs.SyncSourceDir(ctx); err != nil {
a.logger.Warn("document sync failed", "error", err)
} else {
a.logger.Debug("document synced", "path", path)
}
})
watcher.Run(ctx)
}
func (a *App) syncPoll(ctx context.Context) {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {