v0.3.1: CSS jetzt extern via <link> statt JS-injected

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>
This commit is contained in:
2026-07-16 18:23:58 +00:00
co-authored by Claude
parent 4bb8134b9c
commit 7be4656106
3 changed files with 190 additions and 142 deletions
+43 -142
View File
@@ -1,6 +1,6 @@
/* HermesCustom — Lukas' additions to obsidian-web-viewer
*
* Loaded by vault.html (one-line patch) AFTER the main script. Initializes
* Loaded by vault.html via <script> tag (one-line patch). Initializes
* automatically via HermesCustom.init() at the bottom of this file.
*
* Features:
@@ -9,12 +9,9 @@
* 3. Responsive layout (graph collapses below 1024px, tree below 768px)
* 4. URL-based navigation (?file=<path>)
*
* Design notes: see docs/ARCHITECTURE.md
* Companion file: hermes-custom.css (loaded externally via <link>)
*
* IMPORTANT: This script runs AFTER obv's main script (which sets up the
* layout). So our responsiveLayout() needs to apply classes/styles AFTER
* obv's setup, and our CSS must win the specificity battle against obv's
* inline styles + media queries.
* Design notes: see docs/ARCHITECTURE.md
*/
(function() {
'use strict';
@@ -33,155 +30,39 @@
};
// ============================================================
// 1. CSS injection — high specificity to win against obv
// 1. CSS injection — now no-op since CSS is loaded externally
// ============================================================
/**
* Inject responsive CSS into <head> as a stylesheet (not <style>), so
* the cascade is well-defined. We use:
* - High specificity selectors (extra class chains)
* - !important on critical layout properties
* - CSS variables that we toggle via JS
*
* Strategy: we add a class `hermes-rc` (hermes responsive container) to
* the .vault-app element. All our CSS rules start with `.vault-app.hermes-rc`
* to beat obv's `.vault-app` selectors.
* 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() {
if (document.getElementById('hermes-custom-css')) return;
// Wait until <head> exists
const head = document.head || document.getElementsByTagName('head')[0];
if (!head) {
setTimeout(HermesCustom.injectResponsiveCSS, 10);
return;
}
const style = document.createElement('style');
style.id = 'hermes-custom-css';
style.textContent = `
/* === Hermes Custom: always-applied base (no media query) === */
.vault-app.hermes-rc {
box-sizing: border-box !important;
width: 100% !important;
max-width: 100vw !important;
overflow-x: hidden !important; /* prevent horizontal scroll on mobile */
}
.vault-app.hermes-rc .vault-content {
overflow-x: hidden !important; /* content scrolls vertically only */
overflow-y: auto !important;
max-width: 100% !important;
word-wrap: break-word !important; /* long URLs/codes don't overflow */
overflow-wrap: break-word !important;
}
.vault-app.hermes-rc .vault-sidebar {
overflow-x: hidden !important;
overflow-y: auto !important;
max-width: 280px !important;
}
.vault-app.hermes-rc .vault-graph {
overflow: hidden !important;
position: relative !important;
}
/* === Mobile (<768px) === */
.vault-app.hermes-rc.hermes-mobile {
grid-template-columns: 1fr !important;
grid-template-rows: 48px 1fr !important;
}
.vault-app.hermes-rc.hermes-mobile .vault-sidebar {
position: fixed !important;
top: 48px !important;
left: 0 !important;
bottom: 0 !important;
width: 280px !important;
max-width: 85vw !important;
transform: translateX(-100%) !important;
transition: transform 0.2s ease !important;
z-index: 1000 !important;
background: var(--bg-secondary) !important;
border-right: 1px solid var(--border) !important;
display: block !important;
}
.vault-app.hermes-rc.hermes-mobile .vault-sidebar.hermes-sidebar-open {
transform: translateX(0) !important;
}
.vault-app.hermes-rc.hermes-mobile .vault-graph {
display: none !important;
}
.vault-app.hermes-rc.hermes-mobile .vault-content {
grid-column: 1 !important;
padding: 16px 20px !important;
max-width: 100% !important;
}
.hermes-mobile-toggle {
display: none;
background: var(--bg-secondary);
border: 1px solid var(--border);
color: var(--text-secondary);
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-right: 8px;
}
.vault-app.hermes-rc.hermes-mobile .hermes-mobile-toggle { display: inline-block !important; }
/* === Tablet (768px-1023px) === */
.vault-app.hermes-rc.hermes-tablet {
grid-template-columns: 200px 1fr !important;
grid-template-rows: 48px 1fr 220px !important;
height: 100vh !important;
}
.vault-app.hermes-rc.hermes-tablet .vault-sidebar {
width: 200px !important;
max-width: 200px !important;
}
.vault-app.hermes-rc.hermes-tablet .vault-graph {
grid-column: 1 / -1 !important;
grid-row: 3 !important;
height: 220px !important;
border-left: none !important;
border-top: 1px solid var(--border) !important;
}
.vault-app.hermes-rc.hermes-tablet .vault-content {
grid-column: 2 !important;
grid-row: 2 !important;
padding: 20px 28px !important;
}
/* === Desktop (≥1024px): leave obv's default layout alone === */
.vault-app.hermes-rc.hermes-desktop {
/* inherit obv's grid */
}
`;
head.appendChild(style);
console.log('[HermesCustom] CSS injected');
// No-op: CSS loaded externally via <link rel="stylesheet"> in vault.html
};
// ============================================================
// 2. Layout application — called on init and on resize
// 2. Layout application
// ============================================================
HermesCustom.applyLayout = function() {
const app = document.querySelector('.vault-app');
if (!app) {
// obv not loaded yet, retry
setTimeout(HermesCustom.applyLayout, 100);
return;
}
// Add our marker class (specificity booster)
// Add marker class (specificity booster)
if (!app.classList.contains('hermes-rc')) {
app.classList.add('hermes-rc');
}
const width = window.innerWidth;
// Remove all mode classes
app.classList.remove('hermes-mobile', 'hermes-tablet', 'hermes-desktop');
// Sidebar state reset
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');
@@ -191,7 +72,6 @@
app.classList.add('hermes-desktop');
}
// Add hamburger toggle in mobile mode
if (width < BREAKPOINTS.mobile) {
HermesCustom.ensureHamburgerButton();
}
@@ -201,6 +81,19 @@
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 = '☰';
@@ -208,12 +101,16 @@
btn.onclick = (e) => {
e.stopPropagation();
const sidebar = document.querySelector('.vault-sidebar');
if (sidebar) sidebar.classList.toggle('hermes-sidebar-open');
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);
};
// Throttled resize handler
HermesCustom.setupResizeHandler = function() {
let resizeTimer;
let lastWidth = window.innerWidth;
@@ -247,10 +144,9 @@
if (!currentFile) return [];
const graphData = HermesCustom._graphData;
if (!graphData) return [];
const connections = graphData.edges
return graphData.edges
.filter(e => e.source === currentFile)
.map(e => e.target);
return connections;
};
HermesCustom.highlightCurrentNode = function() {
@@ -272,11 +168,15 @@
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);
@@ -331,7 +231,7 @@
};
// ============================================================
// 6. loadFile hook — track current file
// 6. loadFile hook
// ============================================================
HermesCustom.hookLoadFile = function() {
@@ -346,6 +246,11 @@
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;
};
@@ -354,7 +259,7 @@
};
// ============================================================
// 7. Graph data fetch (for highlight logic)
// 7. Graph data fetch
// ============================================================
HermesCustom.loadGraphData = function() {
@@ -373,14 +278,12 @@
// ============================================================
HermesCustom.init = function() {
console.log('[HermesCustom] init v2 (responsive-fix)');
console.log('[HermesCustom] init v3 (CSS external + responsive-layout)');
HermesCustom.injectResponsiveCSS();
HermesCustom.hookLoadFile();
HermesCustom.applyLayout();
HermesCustom.setupResizeHandler();
HermesCustom.loadGraphData();
// Graph click handler needs Three.js to be ready
setTimeout(() => {
HermesCustom.setupGraphClickHandler();
}, 1500);
@@ -390,8 +293,6 @@
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', HermesCustom.init);
} else {
// DOM already ready — but obv's main script may not have run yet.
// Defer slightly to let obv set up the layout first.
setTimeout(HermesCustom.init, 50);
}