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:
@@ -33,12 +33,18 @@ type indexData struct {
|
||||
}
|
||||
|
||||
type documentData struct {
|
||||
Browser browserData
|
||||
Title string
|
||||
Path string
|
||||
Tags []string
|
||||
Hash string
|
||||
HTML template.HTML
|
||||
Browser browserData
|
||||
Title string
|
||||
Path string
|
||||
Tags []string
|
||||
Hash string
|
||||
HTML template.HTML
|
||||
Breadcrumbs []breadcrumb
|
||||
}
|
||||
|
||||
type breadcrumb struct {
|
||||
Name string
|
||||
URL string
|
||||
}
|
||||
|
||||
type errorData struct {
|
||||
@@ -66,6 +72,12 @@ type browserItem struct {
|
||||
}
|
||||
|
||||
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query().Get("q")
|
||||
if query != "" {
|
||||
s.handleSearchPage(w, r, query)
|
||||
return
|
||||
}
|
||||
|
||||
items, err := s.documents.ListDocuments(r.Context())
|
||||
if err != nil {
|
||||
s.renderError(w, r, http.StatusInternalServerError, err.Error())
|
||||
@@ -84,6 +96,30 @@ func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleSearchPage(w http.ResponseWriter, r *http.Request, query string) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
results, err := s.repository.SearchDocuments(ctx, query)
|
||||
if err != nil {
|
||||
s.logger.Warn("search failed", "error", err)
|
||||
}
|
||||
|
||||
s.renderTemplate(w, http.StatusOK, "search.gohtml", layoutData{
|
||||
Title: "Search: " + query,
|
||||
WebEnabled: s.webEnabled,
|
||||
BodyClass: "page-search",
|
||||
BodyTemplate: "search_content",
|
||||
Data: struct {
|
||||
Query string
|
||||
Results []docs.SearchResult
|
||||
}{
|
||||
Query: query,
|
||||
Results: results,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleDocsIndex(w http.ResponseWriter, r *http.Request) {
|
||||
s.renderDocumentPage(w, r, "")
|
||||
}
|
||||
@@ -116,12 +152,13 @@ func (s *Server) renderDocumentPage(w http.ResponseWriter, r *http.Request, page
|
||||
BodyClass: "page-document",
|
||||
BodyTemplate: "document_content",
|
||||
Data: documentData{
|
||||
Browser: buildBrowser(items, page.Path),
|
||||
Title: page.Title,
|
||||
Path: page.Path,
|
||||
Tags: page.Tags,
|
||||
Hash: page.Hash,
|
||||
HTML: template.HTML(page.HTML),
|
||||
Browser: buildBrowser(items, page.Path),
|
||||
Title: page.Title,
|
||||
Path: page.Path,
|
||||
Tags: page.Tags,
|
||||
Hash: page.Hash,
|
||||
HTML: template.HTML(page.HTML),
|
||||
Breadcrumbs: buildBreadcrumbs(page.Path),
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -147,6 +184,26 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query().Get("q")
|
||||
if query == "" {
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"results": []any{}})
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
results, err := s.repository.SearchDocuments(ctx, query)
|
||||
if err != nil {
|
||||
s.logger.Warn("search failed", "error", err)
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"results": []any{}})
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"results": results})
|
||||
}
|
||||
|
||||
func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseMultipartForm(32 << 20); err != nil {
|
||||
s.renderError(w, r, http.StatusBadRequest, "invalid multipart upload")
|
||||
@@ -264,6 +321,25 @@ func sanitizeFilename(name string) string {
|
||||
return strings.ReplaceAll(name, "\"", "")
|
||||
}
|
||||
|
||||
func (s *Server) handleDocuments(w http.ResponseWriter, r *http.Request) {
|
||||
records, err := s.documents.ListDocuments(r.Context())
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
results := make([]map[string]any, 0, len(records))
|
||||
for _, record := range records {
|
||||
results = append(results, map[string]any{
|
||||
"path": record.Path,
|
||||
"title": record.Title,
|
||||
"hash": record.CurrentHash,
|
||||
})
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{"documents": results})
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, payload any) {
|
||||
writeJSONWithStatus(w, status, payload)
|
||||
}
|
||||
@@ -424,3 +500,32 @@ func shouldHideIndexFile(path string, prefix string) bool {
|
||||
folder := strings.TrimSuffix(path, "/index.md")
|
||||
return folder == prefix
|
||||
}
|
||||
|
||||
func buildBreadcrumbs(path string) []breadcrumb {
|
||||
crumbs := []breadcrumb{
|
||||
{Name: "Content", URL: "/"},
|
||||
}
|
||||
if path == "" {
|
||||
return crumbs
|
||||
}
|
||||
|
||||
cleanPath := strings.TrimSuffix(path, "/index.md")
|
||||
cleanPath = strings.TrimSuffix(cleanPath, ".md")
|
||||
parts := strings.Split(cleanPath, "/")
|
||||
currentPath := ""
|
||||
for _, part := range parts {
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
if currentPath == "" {
|
||||
currentPath = part
|
||||
} else {
|
||||
currentPath = currentPath + "/" + part
|
||||
}
|
||||
crumbs = append(crumbs, breadcrumb{
|
||||
Name: part,
|
||||
URL: "/docs/" + currentPath,
|
||||
})
|
||||
}
|
||||
return crumbs
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user