added graph view!
This commit is contained in:
parent
7faeff1799
commit
5f27fb724a
|
|
@ -1006,9 +1006,10 @@ async def search(q: str):
|
|||
|
||||
@api_router.get("/graph")
|
||||
async def get_graph():
|
||||
"""Get graph data for note visualization with wikilink detection"""
|
||||
"""Get graph data for note visualization with wikilink and markdown link detection"""
|
||||
try:
|
||||
import re
|
||||
import urllib.parse
|
||||
notes = get_all_notes(config['storage']['notes_dir'])
|
||||
nodes = []
|
||||
edges = []
|
||||
|
|
@ -1044,8 +1045,9 @@ async def get_graph():
|
|||
# Find wikilinks: [[target]] or [[target|display]]
|
||||
wikilinks = re.findall(r'\[\[([^\]|]+)(?:\|[^\]]+)?\]\]', content)
|
||||
|
||||
# Find standard markdown internal links: [text](path.md)
|
||||
markdown_links = re.findall(r'\[([^\]]+)\]\(([^\)]+\.md)\)', content)
|
||||
# Find standard markdown internal links: [text](path) - any local path (not http/https)
|
||||
# Match links that don't start with http://, https://, mailto:, #, etc.
|
||||
markdown_links = re.findall(r'\[([^\]]+)\]\((?!https?://|mailto:|#|data:)([^\)]+)\)', content)
|
||||
|
||||
# Process wikilinks
|
||||
for target in wikilinks:
|
||||
|
|
@ -1079,12 +1081,50 @@ async def get_graph():
|
|||
|
||||
# Process markdown links
|
||||
for _, link_path in markdown_links:
|
||||
# Try exact match first, then case-insensitive
|
||||
# Skip anchor-only links and external protocols
|
||||
if not link_path or link_path.startswith('#'):
|
||||
continue
|
||||
|
||||
# Remove anchor part if present (e.g., "note.md#section" -> "note.md")
|
||||
link_path = link_path.split('#')[0]
|
||||
if not link_path:
|
||||
continue
|
||||
|
||||
# Normalize path: remove ./ prefix, handle URL encoding
|
||||
link_path = urllib.parse.unquote(link_path)
|
||||
if link_path.startswith('./'):
|
||||
link_path = link_path[2:]
|
||||
|
||||
# Add .md extension if not present and doesn't have other extension
|
||||
link_path_with_md = link_path if link_path.endswith('.md') else link_path + '.md'
|
||||
link_path_lower = link_path.lower()
|
||||
link_path_with_md_lower = link_path_with_md.lower()
|
||||
|
||||
# Try to match target to an existing note
|
||||
target_path = None
|
||||
|
||||
# 1. Exact path match (with or without .md)
|
||||
if link_path in note_paths:
|
||||
target_path = link_path
|
||||
elif link_path.lower() in note_paths_lower:
|
||||
target_path = note_paths_lower[link_path.lower()]
|
||||
target_path = link_path if link_path.endswith('.md') else link_path + '.md'
|
||||
elif link_path_with_md in note_paths:
|
||||
target_path = link_path_with_md
|
||||
# 2. Case-insensitive path match
|
||||
elif link_path_lower in note_paths_lower:
|
||||
target_path = note_paths_lower[link_path_lower]
|
||||
elif link_path_with_md_lower in note_paths_lower:
|
||||
target_path = note_paths_lower[link_path_with_md_lower]
|
||||
# 3. Try matching by filename only (for relative links)
|
||||
else:
|
||||
# Extract just the filename
|
||||
filename = link_path.split('/')[-1]
|
||||
filename_lower = filename.lower()
|
||||
filename_with_md = filename if filename.endswith('.md') else filename + '.md'
|
||||
filename_with_md_lower = filename_with_md.lower()
|
||||
|
||||
if filename_lower in note_names:
|
||||
target_path = note_names[filename_lower]
|
||||
elif filename_with_md_lower in note_names:
|
||||
target_path = note_names[filename_with_md_lower]
|
||||
|
||||
if target_path and target_path != note['path']:
|
||||
edges.append({
|
||||
|
|
|
|||
313
frontend/app.js
313
frontend/app.js
|
|
@ -42,6 +42,12 @@ function noteApp() {
|
|||
noteContent: '',
|
||||
viewMode: 'split', // 'edit', 'split', 'preview'
|
||||
searchQuery: '',
|
||||
|
||||
// Graph state (separate overlay, doesn't affect viewMode)
|
||||
showGraph: false,
|
||||
graphInstance: null,
|
||||
graphLoaded: false,
|
||||
graphData: null,
|
||||
searchResults: [],
|
||||
currentSearchHighlight: '', // Track current highlighted search term
|
||||
currentMatchIndex: 0, // Current match being viewed
|
||||
|
|
@ -549,6 +555,11 @@ function noteApp() {
|
|||
this.renderMermaid();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Refresh graph if visible (longer delay to ensure CSS is applied)
|
||||
if (this.showGraph) {
|
||||
setTimeout(() => this.initGraph(), 300);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load theme:', error);
|
||||
}
|
||||
|
|
@ -1055,9 +1066,7 @@ function noteApp() {
|
|||
const icon = isImage ? '🖼️' : '';
|
||||
|
||||
// Click handler
|
||||
const clickHandler = isImage
|
||||
? `viewImage('${note.path.replace(/'/g, "\\'")}')`
|
||||
: `loadNote('${note.path.replace(/'/g, "\\'")}')`;
|
||||
const clickHandler = `openItem('${note.path.replace(/'/g, "\\'")}', '${note.type}')`;
|
||||
|
||||
// Delete handler
|
||||
const deleteHandler = isImage
|
||||
|
|
@ -1431,8 +1440,19 @@ function noteApp() {
|
|||
}
|
||||
},
|
||||
|
||||
// Open a note or image (unified handler for sidebar/homepage clicks)
|
||||
openItem(path, type = 'note', searchHighlight = '') {
|
||||
this.showGraph = false;
|
||||
if (type === 'image' || path.match(/\.(png|jpg|jpeg|gif|webp)$/i)) {
|
||||
this.viewImage(path);
|
||||
} else {
|
||||
this.loadNote(path, true, searchHighlight);
|
||||
}
|
||||
},
|
||||
|
||||
// View an image in the main pane
|
||||
viewImage(imagePath, updateHistory = true) {
|
||||
this.showGraph = false; // Ensure graph is closed
|
||||
this.currentNote = '';
|
||||
this.currentNoteName = '';
|
||||
this.noteContent = '';
|
||||
|
|
@ -3753,6 +3773,7 @@ function noteApp() {
|
|||
|
||||
// Homepage folder navigation methods
|
||||
goToHomepageFolder(folderPath) {
|
||||
this.showGraph = false; // Close graph when navigating
|
||||
this.selectedHomepageFolder = folderPath || '';
|
||||
|
||||
// Clear editor state to show landing page
|
||||
|
|
@ -3774,6 +3795,7 @@ function noteApp() {
|
|||
|
||||
// Navigate to homepage root and clear all editor state
|
||||
goHome() {
|
||||
this.showGraph = false; // Close graph when going home
|
||||
this.selectedHomepageFolder = '';
|
||||
this.currentNote = '';
|
||||
this.currentNoteName = '';
|
||||
|
|
@ -3790,6 +3812,291 @@ function noteApp() {
|
|||
};
|
||||
|
||||
window.history.pushState({ homepageFolder: '' }, '', '/');
|
||||
},
|
||||
|
||||
// ==================== GRAPH VIEW ====================
|
||||
|
||||
// Initialize the graph visualization
|
||||
async initGraph() {
|
||||
// Check if vis is loaded
|
||||
if (typeof vis === 'undefined') {
|
||||
console.error('vis-network library not loaded');
|
||||
return;
|
||||
}
|
||||
|
||||
this.graphLoaded = false;
|
||||
|
||||
try {
|
||||
// Fetch graph data from API
|
||||
const response = await fetch('/api/graph');
|
||||
if (!response.ok) throw new Error('Failed to fetch graph data');
|
||||
const data = await response.json();
|
||||
this.graphData = data;
|
||||
|
||||
// Get container
|
||||
const container = document.getElementById('graph-overlay');
|
||||
if (!container) return;
|
||||
|
||||
// Get theme colors (force reflow to ensure CSS is applied)
|
||||
document.body.offsetHeight; // Force reflow
|
||||
const style = getComputedStyle(document.documentElement);
|
||||
|
||||
// Helper to get CSS variable with fallback
|
||||
const getCssVar = (name, fallback) => {
|
||||
const value = style.getPropertyValue(name).trim();
|
||||
return value || fallback;
|
||||
};
|
||||
|
||||
const accentPrimary = getCssVar('--accent-primary', '#7c3aed');
|
||||
const accentSecondary = getCssVar('--accent-secondary', '#a78bfa');
|
||||
const textPrimary = getCssVar('--text-primary', '#111827');
|
||||
const textSecondary = getCssVar('--text-secondary', '#6b7280');
|
||||
const bgPrimary = getCssVar('--bg-primary', '#ffffff');
|
||||
const bgSecondary = getCssVar('--bg-secondary', '#f3f4f6');
|
||||
const borderColor = getCssVar('--border-primary', '#e5e7eb');
|
||||
|
||||
// Prepare nodes with styling - all nodes same base color
|
||||
const nodes = new vis.DataSet(data.nodes.map(n => ({
|
||||
id: n.id,
|
||||
label: n.label,
|
||||
title: n.id, // Tooltip shows full path
|
||||
color: {
|
||||
background: accentPrimary,
|
||||
border: accentPrimary,
|
||||
highlight: {
|
||||
background: accentPrimary,
|
||||
border: textPrimary // Darker border when selected
|
||||
},
|
||||
hover: {
|
||||
background: accentSecondary,
|
||||
border: accentPrimary
|
||||
}
|
||||
},
|
||||
font: {
|
||||
color: textPrimary,
|
||||
size: 12,
|
||||
face: 'system-ui, -apple-system, sans-serif'
|
||||
},
|
||||
borderWidth: this.currentNote === n.id ? 4 : 2,
|
||||
chosen: {
|
||||
node: (values) => {
|
||||
values.size = 22;
|
||||
values.borderWidth = 4;
|
||||
values.borderColor = textPrimary;
|
||||
}
|
||||
}
|
||||
})));
|
||||
|
||||
// Prepare edges with styling based on type
|
||||
const edges = new vis.DataSet(data.edges.map((e, i) => ({
|
||||
id: i,
|
||||
from: e.source,
|
||||
to: e.target,
|
||||
color: {
|
||||
color: e.type === 'wikilink' ? accentPrimary : borderColor,
|
||||
highlight: accentPrimary,
|
||||
hover: accentSecondary,
|
||||
opacity: 0.8
|
||||
},
|
||||
width: e.type === 'wikilink' ? 2 : 1,
|
||||
smooth: {
|
||||
type: 'continuous',
|
||||
roundness: 0.5
|
||||
},
|
||||
chosen: {
|
||||
edge: (values) => {
|
||||
values.width = 3;
|
||||
values.color = accentPrimary;
|
||||
}
|
||||
}
|
||||
})));
|
||||
|
||||
// Network options
|
||||
const options = {
|
||||
nodes: {
|
||||
shape: 'dot',
|
||||
size: 16,
|
||||
borderWidth: 2,
|
||||
shadow: {
|
||||
enabled: true,
|
||||
color: 'rgba(0,0,0,0.1)',
|
||||
size: 5,
|
||||
x: 2,
|
||||
y: 2
|
||||
}
|
||||
},
|
||||
edges: {
|
||||
arrows: {
|
||||
to: {
|
||||
enabled: true,
|
||||
scaleFactor: 0.5,
|
||||
type: 'arrow'
|
||||
}
|
||||
}
|
||||
},
|
||||
physics: {
|
||||
enabled: true,
|
||||
solver: 'forceAtlas2Based',
|
||||
forceAtlas2Based: {
|
||||
gravitationalConstant: -50,
|
||||
centralGravity: 0.01,
|
||||
springLength: 100,
|
||||
springConstant: 0.08,
|
||||
damping: 0.4,
|
||||
avoidOverlap: 0.5
|
||||
},
|
||||
stabilization: {
|
||||
enabled: true,
|
||||
iterations: 200,
|
||||
updateInterval: 25
|
||||
}
|
||||
},
|
||||
interaction: {
|
||||
hover: true,
|
||||
tooltipDelay: 200,
|
||||
navigationButtons: false, // Using custom buttons instead
|
||||
keyboard: {
|
||||
enabled: true,
|
||||
bindToWindow: false
|
||||
},
|
||||
zoomView: true,
|
||||
dragView: true
|
||||
},
|
||||
layout: {
|
||||
improvedLayout: true,
|
||||
randomSeed: 42
|
||||
}
|
||||
};
|
||||
|
||||
// Destroy existing instance if any
|
||||
if (this.graphInstance) {
|
||||
this.graphInstance.destroy();
|
||||
this.graphInstance = null;
|
||||
}
|
||||
|
||||
// Clear container to ensure clean state
|
||||
const graphCanvas = container.querySelector('canvas');
|
||||
if (graphCanvas) graphCanvas.remove();
|
||||
const visElements = container.querySelectorAll('.vis-network, .vis-navigation');
|
||||
visElements.forEach(el => el.remove());
|
||||
|
||||
// Create the network
|
||||
this.graphInstance = new vis.Network(container, { nodes, edges }, options);
|
||||
|
||||
// Store reference for callbacks
|
||||
const graphRef = this.graphInstance;
|
||||
const currentNoteRef = this.currentNote;
|
||||
|
||||
// Wait for stabilization
|
||||
this.graphInstance.once('stabilizationIterationsDone', () => {
|
||||
graphRef.setOptions({ physics: { enabled: false } });
|
||||
this.graphLoaded = true;
|
||||
|
||||
// Focus and select current note if one is loaded
|
||||
if (currentNoteRef) {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (graphRef && this.showGraph) {
|
||||
const nodeIds = graphRef.body.data.nodes.getIds();
|
||||
if (nodeIds.includes(currentNoteRef)) {
|
||||
// Focus on the node
|
||||
graphRef.focus(currentNoteRef, {
|
||||
scale: 1.2,
|
||||
animation: {
|
||||
duration: 500,
|
||||
easingFunction: 'easeInOutQuad'
|
||||
}
|
||||
});
|
||||
// Select the node to highlight it
|
||||
graphRef.selectNodes([currentNoteRef]);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore - graph may have been destroyed
|
||||
}
|
||||
}, 150);
|
||||
}
|
||||
});
|
||||
|
||||
// Click event - open note
|
||||
this.graphInstance.on('click', (params) => {
|
||||
if (params.nodes.length > 0) {
|
||||
const noteId = params.nodes[0];
|
||||
this.loadNote(noteId);
|
||||
// Node is already selected by vis-network on click, no need to call selectNodes
|
||||
}
|
||||
});
|
||||
|
||||
// Double-click event - open note and close graph
|
||||
this.graphInstance.on('doubleClick', (params) => {
|
||||
if (params.nodes.length > 0) {
|
||||
const noteId = params.nodes[0];
|
||||
// Close graph and load note
|
||||
this.showGraph = false;
|
||||
this.loadNote(noteId);
|
||||
}
|
||||
});
|
||||
|
||||
// Hover event - highlight connections
|
||||
this.graphInstance.on('hoverNode', (params) => {
|
||||
const nodeId = params.node;
|
||||
const connectedNodes = this.graphInstance.getConnectedNodes(nodeId);
|
||||
const connectedEdges = this.graphInstance.getConnectedEdges(nodeId);
|
||||
|
||||
// Dim all nodes except hovered and connected
|
||||
const allNodes = nodes.getIds();
|
||||
const updates = allNodes.map(id => ({
|
||||
id,
|
||||
opacity: (id === nodeId || connectedNodes.includes(id)) ? 1 : 0.2
|
||||
}));
|
||||
nodes.update(updates);
|
||||
});
|
||||
|
||||
this.graphInstance.on('blurNode', () => {
|
||||
// Reset all nodes to full opacity
|
||||
const allNodes = nodes.getIds();
|
||||
const updates = allNodes.map(id => ({ id, opacity: 1 }));
|
||||
nodes.update(updates);
|
||||
});
|
||||
|
||||
// Add legend to container
|
||||
this.addGraphLegend(container, accentPrimary, borderColor, textSecondary);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize graph:', error);
|
||||
this.graphLoaded = true; // Stop loading indicator
|
||||
}
|
||||
},
|
||||
|
||||
// Add legend to graph container
|
||||
addGraphLegend(container, wikiColor, mdColor, textColor) {
|
||||
// Remove existing legend if any
|
||||
const existingLegend = container.querySelector('.graph-legend');
|
||||
if (existingLegend) existingLegend.remove();
|
||||
|
||||
const legend = document.createElement('div');
|
||||
legend.className = 'graph-legend';
|
||||
legend.innerHTML = `
|
||||
<div class="graph-legend-item">
|
||||
<span class="graph-legend-dot" style="background: ${wikiColor};"></span>
|
||||
<span style="color: ${textColor};">Wikilinks</span>
|
||||
</div>
|
||||
<div class="graph-legend-item">
|
||||
<span class="graph-legend-dot" style="background: ${mdColor};"></span>
|
||||
<span style="color: ${textColor};">Markdown links</span>
|
||||
</div>
|
||||
<div style="margin-top: 8px; font-size: 10px; color: ${textColor}; opacity: 0.7;">
|
||||
Click: select • Double-click: open
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(legend);
|
||||
},
|
||||
|
||||
// Refresh graph when theme changes
|
||||
refreshGraph() {
|
||||
if (this.viewMode === 'graph' && this.graphInstance) {
|
||||
this.initGraph();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,9 @@
|
|||
window.mermaid = mermaid;
|
||||
</script>
|
||||
|
||||
<!-- vis-network for graph visualization (v9.1.9) -->
|
||||
<script src="https://unpkg.com/vis-network@9.1.9/standalone/umd/vis-network.min.js"></script>
|
||||
|
||||
<!-- Theme styles will be loaded dynamically -->
|
||||
<link rel="stylesheet" id="theme-stylesheet" href="">
|
||||
|
||||
|
|
@ -308,6 +311,42 @@
|
|||
border-bottom-style: solid;
|
||||
}
|
||||
|
||||
/* Graph View */
|
||||
#graph-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
#graph-overlay .vis-network {
|
||||
outline: none;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
.graph-legend {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-primary);
|
||||
z-index: 10;
|
||||
}
|
||||
.graph-legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
.graph-legend-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Standard internal links (non-wikilink, e.g. [text](path)) */
|
||||
.markdown-preview a:not([href^="http"]):not([href^="https"]):not([href^="//"]):not([href^="mailto:"]):not([data-wikilink]) {
|
||||
color: var(--accent-primary);
|
||||
|
|
@ -948,7 +987,7 @@
|
|||
</div>
|
||||
<template x-for="note in searchResults" :key="note.path">
|
||||
<div
|
||||
@click="loadNote(note.path, true, searchQuery)"
|
||||
@click="openItem(note.path, note.type, searchQuery)"
|
||||
class="px-3 py-2 mb-1 text-sm rounded cursor-pointer"
|
||||
:style="currentNote === note.path ? 'background-color: var(--accent-light); color: var(--accent-primary);' : 'color: var(--text-primary);'"
|
||||
onmouseover="if(this.style.backgroundColor !== 'var(--accent-light)') this.style.backgroundColor='var(--bg-hover)'"
|
||||
|
|
@ -1061,7 +1100,7 @@
|
|||
draggable="true"
|
||||
@dragstart="onNoteDragStart(note.path, $event)"
|
||||
@dragend="onNoteDragEnd()"
|
||||
@click="note.type === 'image' ? viewImage(note.path) : loadNote(note.path)"
|
||||
@click="openItem(note.path, note.type)"
|
||||
class="note-item px-3 py-2 mb-1 text-sm rounded relative border-2 border-transparent"
|
||||
:style="((note.type === 'image' ? currentImage === note.path : currentNote === note.path) ? 'background-color: var(--accent-light); color: var(--accent-primary);' : 'color: var(--text-primary);') + (note.type === 'image' ? ' opacity: 0.85;' : '') + (draggedNote || draggedFolder ? ' cursor: grabbing;' : ' cursor: pointer;')"
|
||||
@mouseover="if(currentNote !== note.path && currentImage !== note.path) $el.style.backgroundColor='var(--bg-hover)'"
|
||||
|
|
@ -1096,11 +1135,12 @@
|
|||
|
||||
<!-- Footer -->
|
||||
<div class="flex-shrink-0 p-3 border-t" style="border-color: var(--border-primary);">
|
||||
<!-- Theme Selector (compact) -->
|
||||
<!-- Theme Selector and Graph Button -->
|
||||
<div class="flex gap-2 mb-2">
|
||||
<select
|
||||
x-model="currentTheme"
|
||||
@change="setTheme(currentTheme)"
|
||||
class="w-full px-2 py-1.5 text-xs rounded mb-2 focus:outline-none focus:ring-2 focus:ring-opacity-50"
|
||||
class="flex-1 px-2 py-1.5 text-xs rounded focus:outline-none focus:ring-2 focus:ring-opacity-50"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-primary); cursor: pointer;"
|
||||
title="Select theme"
|
||||
>
|
||||
|
|
@ -1108,6 +1148,15 @@
|
|||
<option :value="theme.id" x-text="theme.name"></option>
|
||||
</template>
|
||||
</select>
|
||||
<button
|
||||
@click="showGraph = true; initGraph()"
|
||||
class="px-2 py-1.5 text-xs rounded transition-colors"
|
||||
:style="showGraph ? 'background-color: var(--accent-primary); color: white;' : 'background-color: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-primary);'"
|
||||
title="View note connections graph"
|
||||
>
|
||||
🔗
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="text-xs text-center" style="color: var(--text-tertiary);">
|
||||
|
|
@ -1139,7 +1188,36 @@
|
|||
></div>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div class="flex-1 flex flex-col overflow-hidden">
|
||||
<div class="flex-1 flex flex-col overflow-hidden relative">
|
||||
|
||||
<!-- Graph View Overlay (works from homepage or editor) -->
|
||||
<div
|
||||
x-show="showGraph"
|
||||
id="graph-overlay"
|
||||
class="absolute inset-0"
|
||||
style="background-color: var(--bg-primary); z-index: 50;"
|
||||
>
|
||||
<!-- Graph renders here via vis-network -->
|
||||
<div x-show="!graphLoaded" class="flex items-center justify-center h-full">
|
||||
<div class="text-center" style="color: var(--text-secondary);">
|
||||
<svg class="animate-spin h-8 w-8 mx-auto mb-3" style="color: var(--accent-primary);" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<p>Loading graph...</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Close button -->
|
||||
<button
|
||||
@click="showGraph = false"
|
||||
class="absolute top-4 left-4 px-3 py-2 text-sm font-medium rounded-lg transition-colors z-10"
|
||||
style="background-color: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border-primary);"
|
||||
onmouseover="this.style.backgroundColor='var(--bg-hover)'"
|
||||
onmouseout="this.style.backgroundColor='var(--bg-secondary)'"
|
||||
>
|
||||
← Back
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<template x-if="!currentNote && !currentImage">
|
||||
<!-- Notes Homepage -->
|
||||
|
|
@ -1309,7 +1387,7 @@
|
|||
<!-- Notes -->
|
||||
<template x-for="note in homepageNotes().slice(0, HOMEPAGE_MAX_NOTES)" :key="note.path">
|
||||
<div
|
||||
@click="note.path.match(/\.(png|jpg|jpeg|gif|webp)$/i) ? (currentImage = note.path, currentNote = '') : loadNote(note.path)"
|
||||
@click="openItem(note.path, note.type)"
|
||||
class="p-4 rounded-lg border-2 cursor-pointer transition-all hover:shadow-lg"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border-primary); min-height: 140px; display: flex; flex-direction: column; position: relative;"
|
||||
onmouseover="this.style.borderColor='var(--accent-primary)'; this.style.transform='translateY(-2px)'; this.querySelector('.card-delete-btn').style.opacity='1';"
|
||||
|
|
@ -1475,11 +1553,11 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Editor/Preview -->
|
||||
<!-- Editor/Preview/Graph -->
|
||||
<div class="flex-1 flex relative" style="min-height: 0;">
|
||||
<!-- Editor -->
|
||||
<div
|
||||
x-show="viewMode === 'edit' || viewMode === 'split'"
|
||||
x-show="!currentImage && (viewMode === 'edit' || viewMode === 'split')"
|
||||
class="flex flex-col"
|
||||
style="min-height: 0;"
|
||||
:style="viewMode === 'split' ? `width: ${editorWidth}%;` : 'width: 100%;'"
|
||||
|
|
@ -1501,7 +1579,7 @@
|
|||
|
||||
<!-- Split view resize handle -->
|
||||
<div
|
||||
x-show="viewMode === 'split'"
|
||||
x-show="!currentImage && viewMode === 'split'"
|
||||
@mousedown="startSplitResize($event)"
|
||||
class="split-resize-handle"
|
||||
style="width: 6px; cursor: col-resize; background: linear-gradient(90deg, transparent 0%, var(--border-secondary) 50%, transparent 100%); transition: all 0.2s; position: relative; z-index: 10; opacity: 0.5;"
|
||||
|
|
|
|||
Loading…
Reference in New Issue