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,100 @@
(function () {
const DB_NAME = "md-hub-cache";
const DB_VERSION = 1;
const STORE_DOCUMENTS = "documents";
const STORE_CONTENT = "content";
function openDB() {
return new Promise(function (resolve, reject) {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = function () {
reject(request.error);
};
request.onsuccess = function () {
resolve(request.result);
};
request.onupgradeneeded = function (event) {
const db = event.target.result;
if (!db.objectStoreNames.contains(STORE_DOCUMENTS)) {
db.createObjectStore(STORE_DOCUMENTS, { keyPath: "path" });
}
if (!db.objectStoreNames.contains(STORE_CONTENT)) {
db.createObjectStore(STORE_CONTENT, { keyPath: "path" });
}
};
});
}
function cacheDocuments(documents) {
return openDB().then(function (db) {
return new Promise(function (resolve, reject) {
const tx = db.transaction(STORE_DOCUMENTS, "readwrite");
const store = tx.objectStore(STORE_DOCUMENTS);
documents.forEach(function (doc) {
store.put(doc);
});
tx.oncomplete = function () {
resolve();
};
tx.onerror = function () {
reject(tx.error);
};
});
});
}
function cacheContent(path, html, title) {
return openDB().then(function (db) {
return new Promise(function (resolve, reject) {
const tx = db.transaction(STORE_CONTENT, "readwrite");
const store = tx.objectStore(STORE_CONTENT);
store.put({ path: path, html: html, title: title, cachedAt: Date.now() });
tx.oncomplete = function () {
resolve();
};
tx.onerror = function () {
reject(tx.error);
};
});
});
}
function getCachedDocuments() {
return openDB().then(function (db) {
return new Promise(function (resolve, reject) {
const tx = db.transaction(STORE_DOCUMENTS, "readonly");
const store = tx.objectStore(STORE_DOCUMENTS);
const request = store.getAll();
request.onsuccess = function () {
resolve(request.result);
};
request.onerror = function () {
reject(request.error);
};
});
});
}
function getCachedContent(path) {
return openDB().then(function (db) {
return new Promise(function (resolve, reject) {
const tx = db.transaction(STORE_CONTENT, "readonly");
const store = tx.objectStore(STORE_CONTENT);
const request = store.get(path);
request.onsuccess = function () {
resolve(request.result);
};
request.onerror = function () {
reject(request.error);
};
});
});
}
window.MDHubCache = {
cacheDocuments: cacheDocuments,
cacheContent: cacheContent,
getCachedDocuments: getCachedDocuments,
getCachedContent: getCachedContent,
};
})();