v0.5: Mobile-Toggle-Pattern statt Bottom-Panel
Drei Probleme mit v0.4 Mobile-Layout: 1. 140px Bottom-Panel frisst vertikalen Platz auf Phone 2. User muss zur 3D-Graph scrollen, die nicht interaktiv nutzbar ist 3. Resize zwischen Mobile/Tablet/Desktop an fixen Breakpoints ist holprig Fix v0.5: Graph wird als Full-Screen-Modal über Toggle-Button gezeigt. Neues Layout-System: - Desktop >=1025px: obv default 3-panel, keine Toggles sichtbar - Compact 601-1024px: 2-panel (Tree + Content), Graph als Toggle-Modal, Toggle-Buttons für Tree UND Graph sichtbar im Header - Mobile <=600px: 1-panel (nur Content), Tree als Hamburger (auto-hide nach File-Selection), Graph als Toggle-Modal Plus Mobile-Overflow-Fix: - word-wrap + overflow-wrap auf Markdown-Content - pre/code: white-space: pre-wrap + word-break: break-word - table: display:block + overflow-x: auto - img: max-width: 100% Graph-Modal: - Full-Screen overlay mit Backdrop - Title-Bar + Close-Button (×) - Three.js re-rendered in modal-canvas (für isolierte Performance) - Click-to-Navigate vom Modal zurück in die Page - ESC-Taste schließt Modal 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+155
-79
@@ -1,25 +1,24 @@
|
|||||||
/* HermesCustom CSS — Lukas' responsive-layout additions for obsidian-web-viewer.
|
/* HermesCustom CSS — Lukas' responsive-layout + Mobile-Toggle-Pattern.
|
||||||
*
|
*
|
||||||
* Loaded via <link rel="stylesheet"> in vault.html (one-line patch)
|
* Loaded via <link rel="stylesheet"> in vault.html.
|
||||||
* BEFORE obv's main <style>, so that our rules apply at first paint
|
* Applied at first paint (no FOUC).
|
||||||
* (no FOUC, no layout shift).
|
|
||||||
*
|
*
|
||||||
* Layout classes (toggled by vault-custom.js):
|
* LAYOUT PHILOSOPHY (v0.5):
|
||||||
* .hermes-rc — base marker (added once)
|
* - Desktop ≥1025px: 3-panel layout, tree visible, graph visible
|
||||||
* .hermes-mobile — <768px viewport (tree=hamburger, graph=bottom-panel)
|
* - Tablet 600-1024px: 2-panel compact, tree visible OR graph visible
|
||||||
* .hermes-tablet — 768-1023px (tree=visible, graph=bottom-panel)
|
* via "Show Graph" button toggle (starts hidden)
|
||||||
* .hermes-desktop — >=1024px (3-panel layout, no overrides)
|
* - Mobile <600px: single column, tree as hamburger, graph as FAB → modal
|
||||||
*
|
*
|
||||||
* Sidebar state:
|
* The key UX change vs v0.4: Graph is NEVER in bottom panel on Mobile
|
||||||
* .hermes-sidebar-open — hamburger toggled (mobile only)
|
* (waste of vertical space). It's a toggle button → full-screen modal.
|
||||||
*
|
|
||||||
* Strategy: every selector starts with `.vault-app.hermes-rc` to win
|
|
||||||
* specificity against obv's `.vault-app` only.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* === Always-on (no media query) === */
|
/* === Always-on === */
|
||||||
.vault-app.hermes-rc {
|
.vault-app.hermes-rc,
|
||||||
|
.vault-app.hermes-rc * {
|
||||||
box-sizing: border-box !important;
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
.vault-app.hermes-rc {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
max-width: 100vw !important;
|
max-width: 100vw !important;
|
||||||
overflow-x: hidden !important;
|
overflow-x: hidden !important;
|
||||||
@@ -31,6 +30,23 @@
|
|||||||
word-wrap: break-word !important;
|
word-wrap: break-word !important;
|
||||||
overflow-wrap: break-word !important;
|
overflow-wrap: break-word !important;
|
||||||
hyphens: auto !important;
|
hyphens: auto !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
}
|
||||||
|
/* Critical: prevent pre/code blocks from overflowing horizontally */
|
||||||
|
.vault-app.hermes-rc .vault-content pre,
|
||||||
|
.vault-app.hermes-rc .vault-content code {
|
||||||
|
white-space: pre-wrap !important;
|
||||||
|
word-break: break-word !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
overflow-x: auto !important;
|
||||||
|
}
|
||||||
|
.vault-app.hermes-rc .vault-content table {
|
||||||
|
display: block !important;
|
||||||
|
overflow-x: auto !important;
|
||||||
|
}
|
||||||
|
.vault-app.hermes-rc .vault-content img {
|
||||||
|
max-width: 100% !important;
|
||||||
|
height: auto !important;
|
||||||
}
|
}
|
||||||
.vault-app.hermes-rc .vault-sidebar {
|
.vault-app.hermes-rc .vault-sidebar {
|
||||||
overflow-x: hidden !important;
|
overflow-x: hidden !important;
|
||||||
@@ -41,38 +57,143 @@
|
|||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hamburger button (hidden by default, shown only in mobile) */
|
/* === HermescCustom UI Elements === */
|
||||||
.hermes-mobile-toggle {
|
.hermes-ui-btn {
|
||||||
display: none;
|
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
margin-right: 8px;
|
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
margin: 0 4px;
|
||||||
|
transition: color 0.15s, border-color 0.15s;
|
||||||
}
|
}
|
||||||
.hermes-mobile-toggle:hover {
|
.hermes-ui-btn:hover {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === Mobile (<768px) — tree hamburger + graph bottom-panel === */
|
/* Hamburger button (hidden by default, shown when tree hidden) */
|
||||||
@media (max-width: 767px) {
|
.hermes-tree-toggle,
|
||||||
|
.hermes-graph-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.hermes-tree-toggle svg,
|
||||||
|
.hermes-graph-toggle svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Graph Modal (mobile graph fullscreen overlay) */
|
||||||
|
.hermes-graph-modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 48px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
z-index: 2000;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.hermes-graph-modal.hermes-modal-open {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.hermes-graph-modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
height: 44px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.hermes-graph-modal-title {
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
.hermes-graph-modal-close {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.hermes-graph-modal-close:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
.hermes-graph-modal-canvas {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.hermes-graph-modal-canvas canvas {
|
||||||
|
display: block;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar backdrop (click-outside-to-close) */
|
||||||
|
.hermes-sidebar-backdrop {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 48px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 900;
|
||||||
|
}
|
||||||
|
.hermes-backdrop-visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Compact Mode (≤1024px) — used for Tablet + Mobile === */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.vault-app.hermes-rc.hermes-compact {
|
||||||
|
grid-template-columns: 220px 1fr !important;
|
||||||
|
grid-template-rows: 48px 1fr !important;
|
||||||
|
height: 100vh !important;
|
||||||
|
}
|
||||||
|
.vault-app.hermes-rc.hermes-compact .vault-sidebar {
|
||||||
|
width: 220px !important;
|
||||||
|
max-width: 220px !important;
|
||||||
|
}
|
||||||
|
.vault-app.hermes-rc.hermes-compact .vault-graph {
|
||||||
|
display: none !important; /* Hidden by default on compact */
|
||||||
|
}
|
||||||
|
/* Show tree-toggle in compact (tree is shown by default; toggle if user wants to hide) */
|
||||||
|
.vault-app.hermes-rc.hermes-compact .hermes-tree-toggle {
|
||||||
|
display: inline-block !important;
|
||||||
|
}
|
||||||
|
/* Show graph-toggle in compact */
|
||||||
|
.vault-app.hermes-rc.hermes-compact .hermes-graph-toggle {
|
||||||
|
display: inline-block !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Mobile Mode (≤600px) — Full mobile UX === */
|
||||||
|
@media (max-width: 600px) {
|
||||||
.vault-app.hermes-rc.hermes-mobile {
|
.vault-app.hermes-rc.hermes-mobile {
|
||||||
grid-template-columns: 1fr !important;
|
grid-template-columns: 1fr !important;
|
||||||
grid-template-rows: 48px 1fr 140px !important;
|
grid-template-rows: 48px 1fr !important;
|
||||||
height: 100vh !important;
|
|
||||||
}
|
}
|
||||||
.vault-app.hermes-rc.hermes-mobile .vault-sidebar {
|
.vault-app.hermes-rc.hermes-mobile .vault-sidebar {
|
||||||
position: fixed !important;
|
position: fixed !important;
|
||||||
top: 48px !important;
|
top: 48px !important;
|
||||||
left: 0 !important;
|
left: 0 !important;
|
||||||
bottom: 0 !important;
|
bottom: 0 !important;
|
||||||
width: 280px !important;
|
width: 85vw !important;
|
||||||
max-width: 85vw !important;
|
max-width: 320px !important;
|
||||||
transform: translateX(-100%) !important;
|
transform: translateX(-100%) !important;
|
||||||
transition: transform 0.25s ease-in-out !important;
|
transition: transform 0.25s ease-in-out !important;
|
||||||
z-index: 1000 !important;
|
z-index: 1000 !important;
|
||||||
@@ -84,63 +205,18 @@
|
|||||||
.vault-app.hermes-rc.hermes-mobile .vault-sidebar.hermes-sidebar-open {
|
.vault-app.hermes-rc.hermes-mobile .vault-sidebar.hermes-sidebar-open {
|
||||||
transform: translateX(0) !important;
|
transform: translateX(0) !important;
|
||||||
}
|
}
|
||||||
.vault-app.hermes-rc.hermes-mobile .vault-graph {
|
|
||||||
grid-column: 1 / -1 !important;
|
|
||||||
grid-row: 3 !important;
|
|
||||||
height: 140px !important;
|
|
||||||
border-left: none !important;
|
|
||||||
border-top: 1px solid var(--border) !important;
|
|
||||||
display: block !important; /* keep visible, just at bottom */
|
|
||||||
}
|
|
||||||
.vault-app.hermes-rc.hermes-mobile .vault-content {
|
.vault-app.hermes-rc.hermes-mobile .vault-content {
|
||||||
grid-column: 1 !important;
|
grid-column: 1 !important;
|
||||||
grid-row: 2 !important;
|
grid-row: 2 !important;
|
||||||
padding: 16px 20px !important;
|
padding: 14px 16px !important;
|
||||||
max-width: 100% !important;
|
max-width: 100% !important;
|
||||||
}
|
}
|
||||||
.vault-app.hermes-rc.hermes-mobile .hermes-mobile-toggle {
|
|
||||||
display: inline-block !important;
|
|
||||||
}
|
|
||||||
/* Backdrop for open hamburger */
|
|
||||||
.hermes-sidebar-backdrop {
|
|
||||||
display: none;
|
|
||||||
position: fixed;
|
|
||||||
top: 48px;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
z-index: 999;
|
|
||||||
}
|
|
||||||
.vault-app.hermes-rc.hermes-mobile .hermes-sidebar-backdrop.hermes-backdrop-visible {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === Tablet (768px-1023px) — tree visible, graph bottom-panel === */
|
/* === Desktop (>=1025px) — default obv layout, no overrides === */
|
||||||
@media (min-width: 768px) and (max-width: 1023px) {
|
@media (min-width: 1025px) {
|
||||||
.vault-app.hermes-rc.hermes-tablet {
|
.vault-app.hermes-rc.hermes-desktop .hermes-tree-toggle,
|
||||||
grid-template-columns: 200px 1fr !important;
|
.vault-app.hermes-rc.hermes-desktop .hermes-graph-toggle {
|
||||||
grid-template-rows: 48px 1fr 200px !important;
|
display: none !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: 200px !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 default layout === */
|
|
||||||
/* No overrides needed; we just mark .hermes-desktop class so JS knows */
|
|
||||||
|
|||||||
+281
-127
@@ -1,48 +1,287 @@
|
|||||||
/* HermesCustom — Lukas' additions to obsidian-web-viewer
|
/* HermesCustom — Lukas' additions to obsidian-web-viewer
|
||||||
*
|
*
|
||||||
* Loaded by vault.html via <script> tag (one-line patch). Initializes
|
* Loaded by vault.html via <script> tag (one-line patch).
|
||||||
* automatically via HermesCustom.init() at the bottom of this file.
|
|
||||||
*
|
*
|
||||||
* Features:
|
* v0.5 — Mobile/Responsive overhaul:
|
||||||
* 1. Current-page highlighting in 3D Graph
|
* - Graph is NEVER a permanent bottom panel (waste of vertical space on phones)
|
||||||
* 2. Click-to-navigate on 3D Graph nodes (Three.js raycasting)
|
* - Graph shown via full-screen modal toggle on Mobile + Tablet
|
||||||
* 3. Responsive layout (graph collapses below 1024px, tree below 768px)
|
* - Tree as hamburger on Mobile, full sidebar on Tablet
|
||||||
* 4. URL-based navigation (?file=<path>)
|
* - Desktop unchanged
|
||||||
*
|
*
|
||||||
* Companion file: hermes-custom.css (loaded externally via <link>)
|
* Companion: hermes-custom.css
|
||||||
*
|
*
|
||||||
* Design notes: see docs/ARCHITECTURE.md
|
* 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() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
window.HermesCustom = window.HermesCustom || {};
|
window.HermesCustom = window.HermesCustom || {};
|
||||||
|
|
||||||
const HermesCustom = window.HermesCustom;
|
const HermesCustom = window.HermesCustom;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Configuration
|
// Config
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
const BREAKPOINTS = {
|
const BREAKPOINTS = {
|
||||||
mobile: 768, // <768px
|
mobile: 600,
|
||||||
tablet: 1024, // 768-1023px
|
compact: 1024,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 1. CSS injection — now no-op since CSS is loaded externally
|
// 1. UI elements (injected into header)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CSS lives in hermes-custom.css (loaded via <link> in vault.html).
|
* Inject toggle buttons into header.
|
||||||
* This method remains as a no-op for backward-compat / fallback.
|
* - 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.injectResponsiveCSS = function() {
|
HermesCustom.injectUI = function() {
|
||||||
// No-op: CSS loaded externally via <link rel="stylesheet"> in vault.html
|
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. Layout application
|
// 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() {
|
HermesCustom.applyLayout = function() {
|
||||||
@@ -52,65 +291,28 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add marker class (specificity booster)
|
// Marker class
|
||||||
if (!app.classList.contains('hermes-rc')) {
|
if (!app.classList.contains('hermes-rc')) {
|
||||||
app.classList.add('hermes-rc');
|
app.classList.add('hermes-rc');
|
||||||
}
|
}
|
||||||
|
|
||||||
const width = window.innerWidth;
|
const width = window.innerWidth;
|
||||||
app.classList.remove('hermes-mobile', 'hermes-tablet', 'hermes-desktop');
|
app.classList.remove('hermes-mobile', 'hermes-compact', '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) {
|
if (width <= BREAKPOINTS.mobile) {
|
||||||
app.classList.add('hermes-mobile');
|
app.classList.add('hermes-mobile');
|
||||||
} else if (width < BREAKPOINTS.tablet) {
|
} else if (width <= BREAKPOINTS.compact) {
|
||||||
app.classList.add('hermes-tablet');
|
app.classList.add('hermes-compact');
|
||||||
} else {
|
} else {
|
||||||
app.classList.add('hermes-desktop');
|
app.classList.add('hermes-desktop');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (width < BREAKPOINTS.mobile) {
|
// Auto-close sidebar on resize to desktop
|
||||||
HermesCustom.ensureHamburgerButton();
|
if (width >= BREAKPOINTS.compact) {
|
||||||
|
HermesCustom.closeTreeSidebar();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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() {
|
HermesCustom.setupResizeHandler = function() {
|
||||||
let resizeTimer;
|
let resizeTimer;
|
||||||
let lastWidth = window.innerWidth;
|
let lastWidth = window.innerWidth;
|
||||||
@@ -126,17 +328,13 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 3. Current-page highlighting
|
// 5. Current-page highlighting (desktop)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
HermesCustom.getCurrentFile = function() {
|
HermesCustom.getCurrentFile = function() {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const fileParam = params.get('file');
|
const fileParam = params.get('file');
|
||||||
if (fileParam) return fileParam;
|
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;
|
return HermesCustom._lastLoadedFile || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -144,9 +342,7 @@
|
|||||||
if (!currentFile) return [];
|
if (!currentFile) return [];
|
||||||
const graphData = HermesCustom._graphData;
|
const graphData = HermesCustom._graphData;
|
||||||
if (!graphData) return [];
|
if (!graphData) return [];
|
||||||
return graphData.edges
|
return graphData.edges.filter(e => e.source === currentFile).map(e => e.target);
|
||||||
.filter(e => e.source === currentFile)
|
|
||||||
.map(e => e.target);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
HermesCustom.highlightCurrentNode = function() {
|
HermesCustom.highlightCurrentNode = function() {
|
||||||
@@ -169,69 +365,33 @@
|
|||||||
mesh.material.emissive.setHex(0xFFD700);
|
mesh.material.emissive.setHex(0xFFD700);
|
||||||
mesh.material.emissiveIntensity = 0.6;
|
mesh.material.emissiveIntensity = 0.6;
|
||||||
mesh.material.opacity = 1.0;
|
mesh.material.opacity = 1.0;
|
||||||
mesh.material.transparent = false;
|
|
||||||
mesh.scale.set(1.5, 1.5, 1.5);
|
mesh.scale.set(1.5, 1.5, 1.5);
|
||||||
} else if (isConnected) {
|
} else if (isConnected) {
|
||||||
mesh.material.color.setHex(0x89b4fa);
|
mesh.material.color.setHex(0x89b4fa);
|
||||||
mesh.material.emissive.setHex(0x89b4fa);
|
mesh.material.emissive.setHex(0x89b4fa);
|
||||||
mesh.material.emissiveIntensity = 0.3;
|
mesh.material.emissiveIntensity = 0.3;
|
||||||
mesh.material.opacity = 1.0;
|
mesh.material.opacity = 1.0;
|
||||||
mesh.material.transparent = false;
|
|
||||||
mesh.scale.set(1.1, 1.1, 1.1);
|
mesh.scale.set(1.1, 1.1, 1.1);
|
||||||
} else {
|
} else {
|
||||||
mesh.material.color.setHex(0x313244);
|
mesh.material.color.setHex(0x6c7086);
|
||||||
mesh.material.emissiveIntensity = 0;
|
mesh.material.emissiveIntensity = 0;
|
||||||
mesh.material.opacity = 0.2;
|
mesh.material.opacity = 0.25;
|
||||||
mesh.material.transparent = true;
|
mesh.material.transparent = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 4. Click-to-navigate on 3D Graph
|
// 6. URL navigation
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
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) {
|
HermesCustom.updateURL = function(path) {
|
||||||
const newURL = `?file=${encodeURIComponent(path)}`;
|
const newURL = '?file=' + encodeURIComponent(path);
|
||||||
window.history.pushState({}, '', newURL);
|
window.history.pushState({}, '', newURL);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 6. loadFile hook
|
// 7. loadFile hook
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
HermesCustom.hookLoadFile = function() {
|
HermesCustom.hookLoadFile = function() {
|
||||||
@@ -245,21 +405,19 @@
|
|||||||
window.loadFile = function(path) {
|
window.loadFile = function(path) {
|
||||||
HermesCustom._lastLoadedFile = path;
|
HermesCustom._lastLoadedFile = path;
|
||||||
HermesCustom.updateURL(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);
|
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);
|
setTimeout(HermesCustom.highlightCurrentNode, 300);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
window.loadFile._hermesPatched = true;
|
window.loadFile._hermesPatched = true;
|
||||||
console.log('[HermesCustom] loadFile() hooked');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 7. Graph data fetch
|
// 8. Graph data fetch
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
HermesCustom.loadGraphData = function() {
|
HermesCustom.loadGraphData = function() {
|
||||||
@@ -267,7 +425,7 @@
|
|||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
HermesCustom._graphData = data;
|
HermesCustom._graphData = data;
|
||||||
console.log(`[HermesCustom] Loaded graph: ${data.nodes.length} nodes, ${data.edges.length} edges`);
|
console.log('[HermesCustom] Graph data: ' + data.nodes.length + ' nodes, ' + data.edges.length + ' edges');
|
||||||
HermesCustom.highlightCurrentNode();
|
HermesCustom.highlightCurrentNode();
|
||||||
})
|
})
|
||||||
.catch(err => console.warn('[HermesCustom] Graph fetch failed:', err));
|
.catch(err => console.warn('[HermesCustom] Graph fetch failed:', err));
|
||||||
@@ -278,18 +436,14 @@
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
HermesCustom.init = function() {
|
HermesCustom.init = function() {
|
||||||
console.log('[HermesCustom] init v3 (CSS external + responsive-layout)');
|
console.log('[HermesCustom] init v5 (mobile-toggle-pattern)');
|
||||||
HermesCustom.injectResponsiveCSS();
|
HermesCustom.injectUI();
|
||||||
HermesCustom.hookLoadFile();
|
HermesCustom.hookLoadFile();
|
||||||
HermesCustom.applyLayout();
|
HermesCustom.applyLayout();
|
||||||
HermesCustom.setupResizeHandler();
|
HermesCustom.setupResizeHandler();
|
||||||
HermesCustom.loadGraphData();
|
HermesCustom.loadGraphData();
|
||||||
setTimeout(() => {
|
|
||||||
HermesCustom.setupGraphClickHandler();
|
|
||||||
}, 1500);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Auto-init
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', HermesCustom.init);
|
document.addEventListener('DOMContentLoaded', HermesCustom.init);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user