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

@@ -6,11 +6,18 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{ .Title }}</title>
<link rel="stylesheet" href="/static/site.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" />
<script defer src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
</head>
<body class="{{ .BodyClass }}">
<header class="site-header">
<div class="site-header__inner">
<a class="site-brand" href="/">MD Hub Secure</a>
<form class="site-search" action="/" method="get">
<input type="search" name="q" placeholder="Search..." aria-label="Search documents" />
<button type="submit" aria-label="Search">🔍</button>
</form>
<nav class="site-nav">
<a href="/">Docs</a>
{{ if .WebEnabled }}<a href="/app/">App</a>{{ end }}
@@ -23,15 +30,23 @@
{{ template "index_content" .Data }}
{{ else if eq .BodyTemplate "document_content" }}
{{ template "document_content" .Data }}
{{ else if eq .BodyTemplate "search_content" }}
{{ template "search_content" .Data }}
{{ else if eq .BodyTemplate "error_content" }}
{{ template "error_content" .Data }}
{{ end }}
</main>
<div class="offline-notice" data-offline-notice hidden>
<span aria-hidden="true" class="offline-icon"></span>
<p>You are offline. Some content may be stale.</p>
</div>
<div class="version-notice" data-version-notice hidden>
<p>A newer version is available.</p>
<button type="button" data-version-reload>Reload</button>
</div>
<script src="/static/cache.js" defer></script>
<script src="/static/realtime.js" defer></script>
<script src="/static/render.js" defer></script>
</body>
</html>
{{ end }}

View File

@@ -1,9 +1,23 @@
{{ define "document.gohtml" }}{{ template "base" . }}{{ end }}
{{ define "document_content" }}
<section class="workspace-shell">
<section class="workspace-shell workspace-shell--document">
{{ template "browser" .Browser }}
<article class="document-shell" data-document-path="{{ .Path }}" data-document-hash="{{ .Hash }}">
<nav class="breadcrumbs" aria-label="Breadcrumb">
<ol>
{{ $lastIdx := sub (len .Breadcrumbs) 1 }}
{{ range $i, $crumb := .Breadcrumbs }}
<li>
{{ if eq $i $lastIdx }}
<span aria-current="page">{{ $crumb.Name }}</span>
{{ else }}
<a href="{{ $crumb.URL }}">{{ $crumb.Name }}</a>
{{ end }}
</li>
{{ end }}
</ol>
</nav>
<div class="document-meta">
<h1>{{ .Title }}</h1>
<dl class="meta-grid">
@@ -40,17 +54,22 @@
{{ define "browser" }}
<nav class="miller-browser" aria-label="Documents">
{{ range .Columns }}
<section class="miller-column" aria-label="{{ .Title }}">
<h2>{{ .Title }}</h2>
<section class="miller-column {{ if eq .Title "Content" }}miller-column--root{{ end }}" aria-label="{{ .Title }}">
<h2 title="{{ .Title }}">
{{ if eq .Title "Content" }}
<span class="browser-icon browser-icon--root" aria-hidden="true"></span>
{{ end }}
<span class="miller-column-title-text">{{ .Title }}</span>
</h2>
<ul>
{{ range .Items }}
<li>
<a class="{{ if .Active }}is-active{{ end }} {{ if .IsFolder }}is-folder{{ end }}" href="{{ .URL }}">
<a class="{{ if .Active }}is-active{{ end }} {{ if .IsFolder }}is-folder{{ end }}" href="{{ .URL }}" title="{{ .Name }}">
<span class="browser-item-label">
<span class="browser-icon {{ if .IsFolder }}browser-icon--folder{{ else }}browser-icon--file{{ end }}" aria-hidden="true"></span>
<span>{{ .Name }}</span>
<span class="browser-icon {{ if .IsFolder }}browser-icon--folder{{ else }}browser-icon--page{{ end }}" aria-hidden="true"></span>
<span class="browser-item-name">{{ .Name }}</span>
</span>
{{ if .IsFolder }}<span aria-hidden="true"></span>{{ end }}
{{ if .IsFolder }}<span class="browser-item-chevron" aria-hidden="true"></span>{{ end }}
</a>
</li>
{{ end }}

View File

@@ -0,0 +1,26 @@
{{ define "search.gohtml" }}{{ template "base" . }}{{ end }}
{{ define "search_content" }}
<section class="workspace-shell workspace-shell--empty">
<section class="search-panel">
<p class="eyebrow">Search Results</p>
<h1>{{ .Query }}</h1>
<p class="lede">{{ len .Results }} document{{ if ne (len .Results) 1 }}s{{ end }} found</p>
{{ if .Results }}
<ul class="search-results">
{{ range .Results }}
<li>
<a href="/docs/{{ trimMd .Path }}">
<span class="search-result-title">{{ .Title }}</span>
<span class="search-result-path">{{ .Path }}</span>
</a>
</li>
{{ end }}
</ul>
{{ else }}
<p>No documents found matching your search.</p>
{{ end }}
</section>
</section>
{{ end }}