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:
100
apps/server/internal/httpserver/static/cache.js
Normal file
100
apps/server/internal/httpserver/static/cache.js
Normal 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,
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user