Files
hermes-wiki-static/vault-custom.js.lukas
T
agentandClaude 5394f7e9bf Rollback to upstream-obv (v0.1 effective)
Vier Customization-Iterationen (v0.2 bis v0.5.1) haben auf Mobile (iOS Safari)
und Chrome Desktop jeweils neue Probleme verursacht statt sie zu lösen.

Rollback entfernt:
- vault.html: Script-Tag für vault-custom.js entfernt (upstream-original)
- server.py: ?file= query-Patch entfernt (upstream-original)
- vault-custom.js: leerer Stub mit Comment (Customizations deaktiviert)
- hermes-custom.css: leerer Stub (CSS deaktiviert)

Was bleibt:
- Repo-Struktur (README, CHANGELOG, ARCHITECTURE, CUSTOMIZATIONS, scripts/)
- Documented history der Customization-Iterationen für späteres Re-Audit
- Backup-Dateien: *.lukas (alle Original-Patches vor diesem Commit)

Begründung: Lukas braucht ein verlässlich funktionierendes Wiki, nicht
endlose Problemlösungs-Schleifen. Basis muss stimmen, bevor neue Features
ausprobiert werden.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-18 04:02:08 +00:00

454 lines
18 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* HermesCustom — Lukas' additions to obsidian-web-viewer
*
* Loaded by vault.html via <script> tag (one-line patch).
*
* v0.5 — Mobile/Responsive overhaul:
* - Graph is NEVER a permanent bottom panel (waste of vertical space on phones)
* - Graph shown via full-screen modal toggle on Mobile + Tablet
* - Tree as hamburger on Mobile, full sidebar on Tablet
* - Desktop unchanged
*
* Companion: hermes-custom.css
*
* Layout decisions per viewport:
* Desktop (>=1025px): 3-panel, obv default, both toggles hidden
* Compact (601-1024px): 2-panel (tree+content), graph hidden by default,
* "Show Graph" button visible, "Tree" toggle visible
* Mobile (<=600px): 1-panel (content only), tree hamburger, graph modal
*/
(function() {
'use strict';
window.HermesCustom = window.HermesCustom || {};
const HermesCustom = window.HermesCustom;
// ============================================================
// Config
// ============================================================
const BREAKPOINTS = {
mobile: 600,
compact: 1024,
};
// ============================================================
// 1. UI elements (injected into header)
// ============================================================
/**
* Inject toggle buttons into header.
* - Tree toggle: only visible when tree is hidden (Compact mode hides tree by default)
* - Graph toggle: only visible when graph is hidden (Compact + Mobile)
* - Search input is already in obv's header; we don't duplicate
*/
HermesCustom.injectUI = function() {
const header = document.querySelector('.vault-header');
if (!header) {
setTimeout(HermesCustom.injectUI, 100);
return;
}
// Graph toggle button (top-right of header, before search)
if (!document.querySelector('.hermes-graph-toggle')) {
const graphBtn = document.createElement('button');
graphBtn.className = 'hermes-graph-toggle hermes-ui-btn';
graphBtn.setAttribute('aria-label', 'Toggle 3D graph');
graphBtn.innerHTML = '⊕ Graph';
graphBtn.onclick = () => HermesCustom.toggleGraphModal();
header.insertBefore(graphBtn, header.querySelector('.search-box') || header.lastChild);
}
// Tree toggle button (left-most in header)
if (!document.querySelector('.hermes-tree-toggle')) {
const treeBtn = document.createElement('button');
treeBtn.className = 'hermes-tree-toggle hermes-ui-btn';
treeBtn.setAttribute('aria-label', 'Toggle file tree');
treeBtn.innerHTML = '☰';
treeBtn.onclick = () => HermesCustom.toggleTreeSidebar();
header.insertBefore(treeBtn, header.firstChild);
}
// Backdrop for tree sidebar
if (!document.querySelector('.hermes-sidebar-backdrop')) {
const bd = document.createElement('div');
bd.className = 'hermes-sidebar-backdrop';
bd.onclick = () => HermesCustom.closeTreeSidebar();
document.body.appendChild(bd);
}
// Graph modal (hidden by default)
if (!document.querySelector('.hermes-graph-modal')) {
HermesCustom.buildGraphModal();
}
};
/**
* Build the graph modal: full-screen overlay that re-uses the
* existing canvas from obv. Strategy: clone the canvas into our modal,
* or move the existing canvas in/out. Simpler approach: create a NEW
* canvas inside the modal, and re-run the graph render there.
*/
HermesCustom.buildGraphModal = function() {
const modal = document.createElement('div');
modal.className = 'hermes-graph-modal';
const modalHeader = document.createElement('div');
modalHeader.className = 'hermes-graph-modal-header';
modalHeader.innerHTML = `
<span class="hermes-graph-modal-title">GRAPH VIEW · Tap a node to navigate</span>
<button class="hermes-graph-modal-close">× Close</button>
`;
modal.appendChild(modalHeader);
const canvasContainer = document.createElement('div');
canvasContainer.className = 'hermes-graph-modal-canvas';
canvasContainer.id = 'hermes-graph-modal-canvas';
modal.appendChild(canvasContainer);
document.body.appendChild(modal);
modal.querySelector('.hermes-graph-modal-close').onclick = () => {
HermesCustom.closeGraphModal();
};
// ESC to close
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && modal.classList.contains('hermes-modal-open')) {
HermesCustom.closeGraphModal();
}
});
};
// ============================================================
// 2. Graph modal open/close
// ============================================================
HermesCustom.toggleGraphModal = function() {
const modal = document.querySelector('.hermes-graph-modal');
if (!modal) return;
if (modal.classList.contains('hermes-modal-open')) {
HermesCustom.closeGraphModal();
} else {
HermesCustom.openGraphModal();
}
};
HermesCustom.openGraphModal = function() {
const modal = document.querySelector('.hermes-graph-modal');
const canvasContainer = document.getElementById('hermes-graph-modal-canvas');
if (!modal || !canvasContainer) return;
// Move the existing obv canvas into our modal
const existingCanvas = document.querySelector('.vault-graph canvas');
if (existingCanvas && existingCanvas.parentElement !== canvasContainer) {
// Clone the canvas instead of moving it (keeps obv's state intact)
canvasContainer.innerHTML = '';
const clonedCanvas = document.createElement('canvas');
clonedCanvas.width = window.innerWidth;
clonedCanvas.height = window.innerHeight - 48 - 44; // viewport minus header+modal-header
canvasContainer.appendChild(clonedCanvas);
// Re-render the graph into our cloned canvas by re-running loadGraph logic
HermesCustom.renderGraphIntoCanvas(clonedCanvas);
}
modal.classList.add('hermes-modal-open');
document.body.style.overflow = 'hidden'; // prevent background scroll
console.log('[HermesCustom] Graph modal opened');
};
HermesCustom.closeGraphModal = function() {
const modal = document.querySelector('.hermes-graph-modal');
if (!modal) return;
modal.classList.remove('hermes-modal-open');
document.body.style.overflow = '';
console.log('[HermesCustom] Graph modal closed');
};
/**
* Re-render obv's graph into a different canvas.
* This is a hack: we re-fetch graph data and re-create a Three.js scene
* because obv's `loadGraph()` creates the scene with a specific canvas.
*
* Note: this duplicates Three.js scene. For mobile-only graph viewing,
* this is acceptable (small perf cost, but isolated to user action).
*/
HermesCustom.renderGraphIntoCanvas = function(canvas) {
if (!window.THREE || !HermesCustom._graphData) {
console.warn('[HermesCustom] Three.js or graph data not ready');
return;
}
const ctx = {
scene: new THREE.Scene(),
camera: null,
renderer: null,
nodeObjects: new Map(),
};
const w = canvas.clientWidth || canvas.width;
const h = canvas.clientHeight || canvas.height;
ctx.camera = new THREE.PerspectiveCamera(60, w / h, 1, 5000);
ctx.camera.position.set(0, 0, 800);
ctx.renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
ctx.renderer.setSize(w, h);
ctx.renderer.setPixelRatio(window.devicePixelRatio || 1);
// Build nodes
const nodes = HermesCustom._graphData.nodes;
const nodeGeometry = new THREE.SphereGeometry(4, 16, 16);
const edges = HermesCustom._graphData.edges;
nodes.forEach(node => {
const mat = new THREE.MeshBasicMaterial({ color: 0x6c7086 });
const mesh = new THREE.Mesh(nodeGeometry, mat);
mesh.userData.nodeId = node.id;
// Random-ish positions (deterministic based on label hash)
const hash = node.label.split('').reduce((a, c) => a + c.charCodeAt(0), 0);
mesh.position.set(
(hash % 100) * 6 - 300,
((hash * 7) % 100) * 4 - 200,
((hash * 13) % 100) * 4 - 200
);
ctx.scene.add(mesh);
ctx.nodeObjects.set(node.id, mesh);
});
// Build edges
edges.forEach(edge => {
const src = ctx.nodeObjects.get(edge.source);
const tgt = ctx.nodeObjects.get(edge.target);
if (!src || !tgt) return;
const lineGeo = new THREE.BufferGeometry().setFromPoints([
src.position, tgt.position
]);
const line = new THREE.Line(lineGeo, new THREE.LineBasicMaterial({
color: 0x45475a, transparent: true, opacity: 0.6
}));
ctx.scene.add(line);
});
// Slow rotation
function animate() {
requestAnimationFrame(animate);
ctx.scene.rotation.y += 0.001;
ctx.renderer.render(ctx.scene, ctx.camera);
}
animate();
// Click handler — re-use the same navigation logic
HermesCustom.setupClickForCanvas(canvas, ctx.camera, ctx.nodeObjects);
};
HermesCustom.setupClickForCanvas = function(canvas, camera, nodeMap) {
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
canvas.addEventListener('click', (event) => {
const rect = canvas.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(Array.from(nodeMap.values()));
if (intersects.length > 0) {
const nodeId = intersects[0].object.userData.nodeId;
if (typeof window.loadFile === 'function') {
HermesCustom.closeGraphModal();
window.loadFile(nodeId);
window.history.pushState({}, '', '?file=' + encodeURIComponent(nodeId));
}
}
});
};
// ============================================================
// 3. Tree sidebar toggle
// ============================================================
HermesCustom.toggleTreeSidebar = function() {
const sidebar = document.querySelector('.vault-sidebar');
const backdrop = document.querySelector('.hermes-sidebar-backdrop');
if (!sidebar) return;
const opening = !sidebar.classList.contains('hermes-sidebar-open');
sidebar.classList.toggle('hermes-sidebar-open', opening);
if (backdrop) backdrop.classList.toggle('hermes-backdrop-visible', opening);
};
HermesCustom.closeTreeSidebar = function() {
const sidebar = document.querySelector('.vault-sidebar');
const backdrop = document.querySelector('.hermes-sidebar-backdrop');
if (sidebar) sidebar.classList.remove('hermes-sidebar-open');
if (backdrop) backdrop.classList.remove('hermes-backdrop-visible');
};
// ============================================================
// 4. Layout decision
// ============================================================
HermesCustom.applyLayout = function() {
const app = document.querySelector('.vault-app');
if (!app) {
setTimeout(HermesCustom.applyLayout, 100);
return;
}
// Marker class
if (!app.classList.contains('hermes-rc')) {
app.classList.add('hermes-rc');
}
const width = window.innerWidth;
app.classList.remove('hermes-mobile', 'hermes-compact', 'hermes-desktop');
if (width <= BREAKPOINTS.mobile) {
app.classList.add('hermes-mobile');
} else if (width <= BREAKPOINTS.compact) {
app.classList.add('hermes-compact');
} else {
app.classList.add('hermes-desktop');
}
// Auto-close sidebar on resize to desktop
if (width >= BREAKPOINTS.compact) {
HermesCustom.closeTreeSidebar();
}
};
HermesCustom.setupResizeHandler = function() {
let resizeTimer;
let lastWidth = window.innerWidth;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
if (window.innerWidth !== lastWidth) {
lastWidth = window.innerWidth;
HermesCustom.applyLayout();
}
}, 150);
});
};
// ============================================================
// 5. Current-page highlighting (desktop)
// ============================================================
HermesCustom.getCurrentFile = function() {
const params = new URLSearchParams(window.location.search);
const fileParam = params.get('file');
if (fileParam) return fileParam;
return HermesCustom._lastLoadedFile || null;
};
HermesCustom.getConnectedFiles = function(currentFile) {
if (!currentFile) return [];
const graphData = HermesCustom._graphData;
if (!graphData) return [];
return graphData.edges.filter(e => e.source === currentFile).map(e => e.target);
};
HermesCustom.highlightCurrentNode = function() {
const currentFile = HermesCustom.getCurrentFile();
if (!currentFile) return;
if (typeof window.graphNodeObjects === 'undefined' || !window.graphNodeObjects) {
setTimeout(HermesCustom.highlightCurrentNode, 500);
return;
}
const connected = new Set(HermesCustom.getConnectedFiles(currentFile));
connected.add(currentFile);
window.graphNodeObjects.forEach((mesh, nodeId) => {
const isCurrent = (nodeId === currentFile);
const isConnected = connected.has(nodeId) && !isCurrent;
if (isCurrent) {
mesh.material.color.setHex(0xFFD700);
mesh.material.emissive.setHex(0xFFD700);
mesh.material.emissiveIntensity = 0.6;
mesh.material.opacity = 1.0;
mesh.scale.set(1.5, 1.5, 1.5);
} else if (isConnected) {
mesh.material.color.setHex(0x89b4fa);
mesh.material.emissive.setHex(0x89b4fa);
mesh.material.emissiveIntensity = 0.3;
mesh.material.opacity = 1.0;
mesh.scale.set(1.1, 1.1, 1.1);
} else {
mesh.material.color.setHex(0x6c7086);
mesh.material.emissiveIntensity = 0;
mesh.material.opacity = 0.25;
mesh.material.transparent = true;
}
});
};
// ============================================================
// 6. URL navigation
// ============================================================
HermesCustom.updateURL = function(path) {
const newURL = '?file=' + encodeURIComponent(path);
window.history.pushState({}, '', newURL);
};
// ============================================================
// 7. loadFile hook
// ============================================================
HermesCustom.hookLoadFile = function() {
if (typeof window.loadFile !== 'function') {
setTimeout(HermesCustom.hookLoadFile, 200);
return;
}
if (window.loadFile._hermesPatched) return;
const originalLoadFile = window.loadFile;
window.loadFile = function(path) {
HermesCustom._lastLoadedFile = path;
HermesCustom.updateURL(path);
// Close tree sidebar after file selection (mobile UX)
if (window.innerWidth <= BREAKPOINTS.mobile) {
HermesCustom.closeTreeSidebar();
}
const result = originalLoadFile.apply(this, arguments);
setTimeout(HermesCustom.highlightCurrentNode, 300);
return result;
};
window.loadFile._hermesPatched = true;
};
// ============================================================
// 8. Graph data fetch
// ============================================================
HermesCustom.loadGraphData = function() {
fetch('/api/vault/graph')
.then(r => r.json())
.then(data => {
HermesCustom._graphData = data;
console.log('[HermesCustom] Graph data: ' + data.nodes.length + ' nodes, ' + data.edges.length + ' edges');
HermesCustom.highlightCurrentNode();
})
.catch(err => console.warn('[HermesCustom] Graph fetch failed:', err));
};
// ============================================================
// Init
// ============================================================
HermesCustom.init = function() {
console.log('[HermesCustom] init v5 (mobile-toggle-pattern)');
HermesCustom.injectUI();
HermesCustom.hookLoadFile();
HermesCustom.applyLayout();
HermesCustom.setupResizeHandler();
HermesCustom.loadGraphData();
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', HermesCustom.init);
} else {
setTimeout(HermesCustom.init, 50);
}
})();