Drei Probleme mit der JS-Injection: 1. CSS kommt NACH First Paint → User sieht kurze obv-Layout, dann 'springt' zu responsive Layout (FOUC = Flash of Unstyled Content) 2. JS-Injection kann fehlschlagen (head nicht ready, Browser-Inkonsistenzen) 3. Inline <style>-Tag wird nicht gecacht, lädt bei jedem Reload Fix: hermes-custom.css als separate Datei, geladen via <link rel='stylesheet'> im HEAD. Server liefert sie automatisch. Außerdem: Mobile-Mode zeigt Graph als Bottom-Panel (140px), nicht mehr display:none. User behält Wiki-Beziehungen auch auf Phone. vault-custom.js.js behält injectResponsiveCSS als no-op für Backward-Compat. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
300 lines
11 KiB
JavaScript
300 lines
11 KiB
JavaScript
/* HermesCustom — Lukas' additions to obsidian-web-viewer
|
|
*
|
|
* Loaded by vault.html via <script> tag (one-line patch). Initializes
|
|
* automatically via HermesCustom.init() at the bottom of this file.
|
|
*
|
|
* Features:
|
|
* 1. Current-page highlighting in 3D Graph
|
|
* 2. Click-to-navigate on 3D Graph nodes (Three.js raycasting)
|
|
* 3. Responsive layout (graph collapses below 1024px, tree below 768px)
|
|
* 4. URL-based navigation (?file=<path>)
|
|
*
|
|
* Companion file: hermes-custom.css (loaded externally via <link>)
|
|
*
|
|
* Design notes: see docs/ARCHITECTURE.md
|
|
*/
|
|
(function() {
|
|
'use strict';
|
|
|
|
window.HermesCustom = window.HermesCustom || {};
|
|
|
|
const HermesCustom = window.HermesCustom;
|
|
|
|
// ============================================================
|
|
// Configuration
|
|
// ============================================================
|
|
|
|
const BREAKPOINTS = {
|
|
mobile: 768, // <768px
|
|
tablet: 1024, // 768-1023px
|
|
};
|
|
|
|
// ============================================================
|
|
// 1. CSS injection — now no-op since CSS is loaded externally
|
|
// ============================================================
|
|
|
|
/**
|
|
* CSS lives in hermes-custom.css (loaded via <link> in vault.html).
|
|
* This method remains as a no-op for backward-compat / fallback.
|
|
*/
|
|
HermesCustom.injectResponsiveCSS = function() {
|
|
// No-op: CSS loaded externally via <link rel="stylesheet"> in vault.html
|
|
};
|
|
|
|
// ============================================================
|
|
// 2. Layout application
|
|
// ============================================================
|
|
|
|
HermesCustom.applyLayout = function() {
|
|
const app = document.querySelector('.vault-app');
|
|
if (!app) {
|
|
setTimeout(HermesCustom.applyLayout, 100);
|
|
return;
|
|
}
|
|
|
|
// Add marker class (specificity booster)
|
|
if (!app.classList.contains('hermes-rc')) {
|
|
app.classList.add('hermes-rc');
|
|
}
|
|
|
|
const width = window.innerWidth;
|
|
app.classList.remove('hermes-mobile', 'hermes-tablet', 'hermes-desktop');
|
|
const sidebar = document.querySelector('.vault-sidebar');
|
|
if (sidebar) sidebar.classList.remove('hermes-sidebar-open');
|
|
const backdrop = document.querySelector('.hermes-sidebar-backdrop');
|
|
if (backdrop) backdrop.classList.remove('hermes-backdrop-visible');
|
|
|
|
if (width < BREAKPOINTS.mobile) {
|
|
app.classList.add('hermes-mobile');
|
|
} else if (width < BREAKPOINTS.tablet) {
|
|
app.classList.add('hermes-tablet');
|
|
} else {
|
|
app.classList.add('hermes-desktop');
|
|
}
|
|
|
|
if (width < BREAKPOINTS.mobile) {
|
|
HermesCustom.ensureHamburgerButton();
|
|
}
|
|
};
|
|
|
|
HermesCustom.ensureHamburgerButton = function() {
|
|
if (document.querySelector('.hermes-mobile-toggle')) return;
|
|
const header = document.querySelector('.vault-header');
|
|
if (!header) return;
|
|
|
|
// Backdrop (click-outside-to-close)
|
|
if (!document.querySelector('.hermes-sidebar-backdrop')) {
|
|
const backdrop = document.createElement('div');
|
|
backdrop.className = 'hermes-sidebar-backdrop';
|
|
backdrop.onclick = () => {
|
|
const sidebar = document.querySelector('.vault-sidebar');
|
|
if (sidebar) sidebar.classList.remove('hermes-sidebar-open');
|
|
backdrop.classList.remove('hermes-backdrop-visible');
|
|
};
|
|
document.body.appendChild(backdrop);
|
|
}
|
|
|
|
const btn = document.createElement('button');
|
|
btn.className = 'hermes-mobile-toggle';
|
|
btn.textContent = '☰';
|
|
btn.setAttribute('aria-label', 'Toggle file tree');
|
|
btn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
const sidebar = document.querySelector('.vault-sidebar');
|
|
const backdrop = document.querySelector('.hermes-sidebar-backdrop');
|
|
if (sidebar) {
|
|
sidebar.classList.toggle('hermes-sidebar-open');
|
|
if (backdrop) backdrop.classList.toggle('hermes-backdrop-visible',
|
|
sidebar.classList.contains('hermes-sidebar-open'));
|
|
}
|
|
};
|
|
header.insertBefore(btn, header.firstChild);
|
|
};
|
|
|
|
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);
|
|
});
|
|
};
|
|
|
|
// ============================================================
|
|
// 3. Current-page highlighting
|
|
// ============================================================
|
|
|
|
HermesCustom.getCurrentFile = function() {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const fileParam = params.get('file');
|
|
if (fileParam) return fileParam;
|
|
if (window.location.hash) {
|
|
const hashMatch = window.location.hash.match(/file=([^&]+)/);
|
|
if (hashMatch) return decodeURIComponent(hashMatch[1]);
|
|
}
|
|
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.material.transparent = false;
|
|
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.material.transparent = false;
|
|
mesh.scale.set(1.1, 1.1, 1.1);
|
|
} else {
|
|
mesh.material.color.setHex(0x313244);
|
|
mesh.material.emissiveIntensity = 0;
|
|
mesh.material.opacity = 0.2;
|
|
mesh.material.transparent = true;
|
|
}
|
|
});
|
|
};
|
|
|
|
// ============================================================
|
|
// 4. Click-to-navigate on 3D Graph
|
|
// ============================================================
|
|
|
|
HermesCustom.setupGraphClickHandler = function() {
|
|
if (!window.graphRenderer || !window.graphScene || !window.graphCamera) {
|
|
setTimeout(HermesCustom.setupGraphClickHandler, 500);
|
|
return;
|
|
}
|
|
|
|
const raycaster = new THREE.Raycaster();
|
|
const mouse = new THREE.Vector2();
|
|
|
|
window.graphRenderer.domElement.addEventListener('click', (event) => {
|
|
const rect = window.graphRenderer.domElement.getBoundingClientRect();
|
|
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
|
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
|
|
|
raycaster.setFromCamera(mouse, window.graphCamera);
|
|
const intersects = raycaster.intersectObjects(
|
|
Object.values(window.graphNodeObjects || {})
|
|
);
|
|
|
|
if (intersects.length > 0) {
|
|
const nodeId = intersects[0].object.userData.nodeId;
|
|
if (nodeId && typeof window.loadFile === 'function') {
|
|
window.loadFile(nodeId);
|
|
window.history.pushState({}, '', `?file=${encodeURIComponent(nodeId)}`);
|
|
}
|
|
}
|
|
});
|
|
console.log('[HermesCustom] Graph click handler attached');
|
|
};
|
|
|
|
// ============================================================
|
|
// 5. URL-based navigation
|
|
// ============================================================
|
|
|
|
HermesCustom.updateURL = function(path) {
|
|
const newURL = `?file=${encodeURIComponent(path)}`;
|
|
window.history.pushState({}, '', newURL);
|
|
};
|
|
|
|
// ============================================================
|
|
// 6. 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);
|
|
const result = originalLoadFile.apply(this, arguments);
|
|
// Close sidebar on mobile after file selection
|
|
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');
|
|
setTimeout(HermesCustom.highlightCurrentNode, 300);
|
|
return result;
|
|
};
|
|
window.loadFile._hermesPatched = true;
|
|
console.log('[HermesCustom] loadFile() hooked');
|
|
};
|
|
|
|
// ============================================================
|
|
// 7. Graph data fetch
|
|
// ============================================================
|
|
|
|
HermesCustom.loadGraphData = function() {
|
|
fetch('/api/vault/graph')
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
HermesCustom._graphData = data;
|
|
console.log(`[HermesCustom] Loaded graph: ${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 v3 (CSS external + responsive-layout)');
|
|
HermesCustom.injectResponsiveCSS();
|
|
HermesCustom.hookLoadFile();
|
|
HermesCustom.applyLayout();
|
|
HermesCustom.setupResizeHandler();
|
|
HermesCustom.loadGraphData();
|
|
setTimeout(() => {
|
|
HermesCustom.setupGraphClickHandler();
|
|
}, 1500);
|
|
};
|
|
|
|
// Auto-init
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', HermesCustom.init);
|
|
} else {
|
|
setTimeout(HermesCustom.init, 50);
|
|
}
|
|
|
|
})();
|