2052 lines
62 KiB
Plaintext
2052 lines
62 KiB
Plaintext
---
|
||
// src/components/KnowledgeGraph.astro
|
||
// Enhanced interactive visualization of content connections using Cytoscape.js
|
||
|
||
export interface GraphNode {
|
||
id: string;
|
||
label: string;
|
||
type: 'post' | 'tag' | 'category'; // Node types to distinguish posts from tags
|
||
category?: string;
|
||
tags?: string[];
|
||
url?: string; // URL for linking
|
||
content?: string; // Add content property for post full content
|
||
}
|
||
|
||
export interface GraphEdge {
|
||
source: string;
|
||
target: string;
|
||
type: 'post-tag' | 'post-category' | 'post-post'; // Edge types
|
||
strength?: number;
|
||
}
|
||
|
||
export interface GraphData {
|
||
nodes: GraphNode[];
|
||
edges: GraphEdge[];
|
||
}
|
||
|
||
interface Props {
|
||
graphData: GraphData;
|
||
height?: string; // e.g., '500px'
|
||
initialFilter?: string; // Optional initial filter
|
||
}
|
||
|
||
const { graphData, height = "50vh", initialFilter = "all" } = Astro.props;
|
||
|
||
// Generate colors based on node types
|
||
const nodeTypeColors = {
|
||
'post': '#3B82F6', // Blue for posts
|
||
'tag': '#10B981', // Green for tags
|
||
'category': '#8B5CF6' // Purple for categories
|
||
};
|
||
|
||
// Generate predefined colors for categories
|
||
const predefinedColors = {
|
||
'Kubernetes': '#326CE5', 'Docker': '#2496ED', 'DevOps': '#FF6F61',
|
||
'Homelab': '#06B6D4', 'Networking': '#9333EA', 'Infrastructure': '#10B981',
|
||
'Automation': '#F59E0B', 'Security': '#EF4444', 'Monitoring': '#6366F1',
|
||
'Storage': '#8B5CF6', 'Obsidian': '#7C3AED', 'Tutorial': '#3B82F6',
|
||
'Uncategorized': '#A0AEC0'
|
||
};
|
||
|
||
// Calculate node sizes
|
||
const nodeSizes = {};
|
||
const minSize = 15; const maxSize = 35;
|
||
const degreeMap = new Map();
|
||
graphData.nodes.forEach(node => degreeMap.set(node.id, 0));
|
||
graphData.edges.forEach(edge => {
|
||
degreeMap.set(edge.source, (degreeMap.get(edge.source) || 0) + 1);
|
||
degreeMap.set(edge.target, (degreeMap.get(edge.target) || 0) + 1);
|
||
});
|
||
const maxDegree = Math.max(...Array.from(degreeMap.values()), 1);
|
||
graphData.nodes.forEach(node => {
|
||
const degree = degreeMap.get(node.id) || 0;
|
||
// Make tags slightly smaller than posts by default
|
||
const baseSize = node.type === 'post' ? minSize : minSize * 0.8;
|
||
const normalizedSize = maxDegree === 0 ? 0.5 : degree / maxDegree;
|
||
nodeSizes[node.id] = baseSize + normalizedSize * (maxSize - minSize);
|
||
});
|
||
|
||
// Count node types for legend
|
||
const nodeTypeCounts = {
|
||
post: graphData.nodes.filter(node => node.type === 'post').length,
|
||
tag: graphData.nodes.filter(node => node.type === 'tag').length,
|
||
category: graphData.nodes.filter(node => node.type === 'category').length
|
||
};
|
||
---
|
||
|
||
<!-- Include Cytoscape via CDN with is:inline to ensure it loads before the script runs -->
|
||
<script src="https://unpkg.com/cytoscape@3.25.0/dist/cytoscape.min.js" is:inline></script>
|
||
|
||
<div class="graph-container-wrapper" style={`--graph-height: ${height};`}>
|
||
<!-- Instructions overlay - move to be positioned absolutely over the graph -->
|
||
<div class="graph-instructions overlay">
|
||
<details class="instructions-details">
|
||
<summary class="instructions-summary">How to use the Knowledge Graph</summary>
|
||
<div class="instructions-content">
|
||
<p>This Knowledge Graph visualizes connections between blog content:</p>
|
||
<ul>
|
||
<li><span class="node-example post-node"></span> <strong>Posts</strong> - Blog articles (circle nodes)</li>
|
||
<li><span class="node-example tag-node"></span> <strong>Tags</strong> - Content topics (diamond nodes)</li>
|
||
</ul>
|
||
<p>Interactions:</p>
|
||
<ul>
|
||
<li>Click a node to see its connections and details</li>
|
||
<li>Click a tag node to filter posts by that tag</li>
|
||
<li>Click a post node to highlight that specific post</li>
|
||
<li>Use the filter buttons below to focus on specific topics</li>
|
||
<li>Use mouse wheel to zoom in/out and drag to pan</li>
|
||
<li>Click an empty area to reset the view</li>
|
||
<li>In fullscreen mode, click a post to view its full content</li>
|
||
</ul>
|
||
</div>
|
||
</details>
|
||
</div>
|
||
|
||
<!-- Loading Animation -->
|
||
<div id="graph-loading" class="graph-loading">
|
||
<div class="loading-spinner">
|
||
<div class="spinner-ring"></div>
|
||
<div class="spinner-ring"></div>
|
||
<div class="spinner-ring"></div>
|
||
</div>
|
||
<div class="loading-text">Initializing Knowledge Graph...</div>
|
||
</div>
|
||
|
||
<!-- Cytoscape Container -->
|
||
<div id="knowledge-graph" class="graph-container"></div>
|
||
|
||
<!-- Node Details Panel -->
|
||
<div id="node-details" class="node-details">
|
||
<div class="node-details-header">
|
||
<h3 id="node-title" class="node-title">Node Title</h3>
|
||
<button id="close-details" class="close-button" aria-label="Close details">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||
</button>
|
||
</div>
|
||
<div id="node-type" class="node-type">
|
||
<span class="type-value">Type</span>
|
||
</div>
|
||
<div id="node-category" class="node-category">
|
||
<span class="category-label">Category:</span>
|
||
<span class="category-value">Category Name</span>
|
||
</div>
|
||
<div id="node-tags" class="node-tags">
|
||
<span class="tags-label">Tags:</span>
|
||
<div class="tags-container">
|
||
<!-- Tags populated by JS -->
|
||
</div>
|
||
</div>
|
||
<div id="node-connections" class="node-connections">
|
||
<span class="connections-label">Connections:</span>
|
||
<ul class="connections-list">
|
||
<!-- Connections populated by JS -->
|
||
</ul>
|
||
</div>
|
||
<a href="#" id="node-link" class="node-link" target="_self" rel="noopener noreferrer">View Content</a>
|
||
</div>
|
||
|
||
<!-- Fullscreen Toggle Button -->
|
||
<button id="fullscreen-toggle" class="fullscreen-toggle" aria-label="Toggle fullscreen">
|
||
<svg id="fullscreen-enter-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<polyline points="15 3 21 3 21 9"></polyline>
|
||
<polyline points="9 21 3 21 3 15"></polyline>
|
||
<line x1="21" y1="3" x2="14" y2="10"></line>
|
||
<line x1="3" y1="21" x2="10" y2="14"></line>
|
||
</svg>
|
||
<svg id="fullscreen-exit-icon" class="hidden" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<polyline points="4 14 10 14 10 20"></polyline>
|
||
<polyline points="20 10 14 10 14 4"></polyline>
|
||
<line x1="14" y1="10" x2="21" y2="3"></line>
|
||
<line x1="3" y1="21" x2="10" y2="14"></line>
|
||
</svg>
|
||
</button>
|
||
|
||
<!-- Full Post Content Panel -->
|
||
<div id="full-post-content" class="full-post-content">
|
||
<div class="full-post-header">
|
||
<h2 id="full-post-title" class="full-post-title">Post Title</h2>
|
||
<button id="close-full-post" class="close-button" aria-label="Close post content">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||
</button>
|
||
</div>
|
||
<div class="full-post-metadata">
|
||
<div class="metadata-row">
|
||
<span class="metadata-label">Category:</span>
|
||
<div id="full-post-category" class="full-post-category"></div>
|
||
</div>
|
||
<div class="metadata-row">
|
||
<span class="metadata-label">Tags:</span>
|
||
<div id="full-post-tags" class="full-post-tags"></div>
|
||
</div>
|
||
</div>
|
||
<div id="full-post-container" class="full-post-container">
|
||
<div class="full-post-placeholder">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||
<polyline points="14 2 14 8 20 8"></polyline>
|
||
<line x1="16" y1="13" x2="8" y2="13"></line>
|
||
<line x1="16" y1="17" x2="8" y2="17"></line>
|
||
<polyline points="10 9 9 9 8 9"></polyline>
|
||
</svg>
|
||
<p>Select a post node to view its content</p>
|
||
</div>
|
||
</div>
|
||
<div class="full-post-footer">
|
||
<a href="#" id="full-post-link" class="full-post-link" target="_self" rel="noopener noreferrer">
|
||
Read Full Article
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
|
||
<polyline points="15 3 21 3 21 9"></polyline>
|
||
<line x1="10" y1="14" x2="21" y2="3"></line>
|
||
</svg>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Graph Controls -->
|
||
<div class="graph-controls">
|
||
<div class="graph-filters">
|
||
<button class="graph-filter active" data-filter="all" style="--filter-color: var(--accent-primary);">All</button>
|
||
<button class="graph-filter" data-filter="posts" style="--filter-color: #3B82F6;">Posts ({nodeTypeCounts.post})</button>
|
||
<button class="graph-filter" data-filter="tags" style="--filter-color: #10B981;">Tags ({nodeTypeCounts.tag})</button>
|
||
</div>
|
||
<div class="graph-actions">
|
||
<button id="zoom-in" class="graph-action" aria-label="Zoom In">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
|
||
</button>
|
||
<button id="zoom-out" class="graph-action" aria-label="Zoom Out">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line></svg>
|
||
</button>
|
||
<button id="reset-graph" class="graph-action" aria-label="Reset View">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 2v6h6"></path><path d="M21 12A9 9 0 0 0 6 5.3L3 8"></path><path d="M21 22v-6h-6"></path><path d="M3 12a9 9 0 0 0 15 6.7l3-2.7"></path></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script define:vars={{ graphData, nodeTypeColors, nodeSizes, predefinedColors, initialFilter }}>
|
||
// Initialize the graph when the DOM is ready
|
||
function initializeGraph() {
|
||
// Check if Cytoscape is loaded
|
||
if (typeof cytoscape === 'undefined') {
|
||
console.error("Cytoscape library not loaded. Make sure it's included via the script tag.");
|
||
const loadingEl = document.getElementById('graph-loading');
|
||
if(loadingEl) loadingEl.innerHTML = "<p>Error: Cytoscape library not loaded.</p>";
|
||
return;
|
||
}
|
||
|
||
const loadingEl = document.getElementById('graph-loading');
|
||
const nodeDetailsEl = document.getElementById('node-details');
|
||
const closeDetailsBtn = document.getElementById('close-details');
|
||
const graphContainer = document.getElementById('knowledge-graph');
|
||
const graphWrapper = document.querySelector('.graph-container-wrapper');
|
||
const fullscreenToggle = document.getElementById('fullscreen-toggle');
|
||
const fullscreenEnterIcon = document.getElementById('fullscreen-enter-icon');
|
||
const fullscreenExitIcon = document.getElementById('fullscreen-exit-icon');
|
||
const fullPostContent = document.getElementById('full-post-content');
|
||
const closeFullPostBtn = document.getElementById('close-full-post');
|
||
const fullPostTitle = document.getElementById('full-post-title');
|
||
const fullPostContainer = document.getElementById('full-post-container');
|
||
const fullPostCategory = document.getElementById('full-post-category');
|
||
const fullPostTags = document.getElementById('full-post-tags');
|
||
const fullPostLink = document.getElementById('full-post-link');
|
||
|
||
if (!graphContainer) {
|
||
console.error("Knowledge graph container not found!");
|
||
return;
|
||
}
|
||
if (!graphData || !graphData.nodes) {
|
||
console.error("Graph data is missing or invalid.");
|
||
if(loadingEl) loadingEl.innerHTML = "<p>Error loading graph data.</p>";
|
||
return;
|
||
}
|
||
|
||
// Format data for Cytoscape
|
||
const elements = [];
|
||
|
||
// State variables
|
||
let isFullscreen = false;
|
||
|
||
// Add nodes with appropriate styling based on type
|
||
graphData.nodes.forEach(node => {
|
||
let nodeColor;
|
||
|
||
if (node.type === 'post') {
|
||
// Posts get category color if available
|
||
nodeColor = node.category && predefinedColors[node.category]
|
||
? predefinedColors[node.category]
|
||
: nodeTypeColors['post'];
|
||
} else {
|
||
// Tags and categories get their type color
|
||
nodeColor = nodeTypeColors[node.type];
|
||
}
|
||
|
||
elements.push({
|
||
data: {
|
||
id: node.id,
|
||
label: node.label,
|
||
type: node.type,
|
||
category: node.category || '',
|
||
tags: node.tags || [],
|
||
size: nodeSizes[node.id] || 25,
|
||
color: nodeColor,
|
||
url: node.url || '#',
|
||
content: node.content || '' // Include content data for post nodes
|
||
}
|
||
});
|
||
});
|
||
|
||
// Add edges
|
||
graphData.edges.forEach((edge, index) => {
|
||
if (graphData.nodes.some(n => n.id === edge.source) && graphData.nodes.some(n => n.id === edge.target)) {
|
||
elements.push({
|
||
data: {
|
||
id: `e${index}`,
|
||
source: edge.source,
|
||
target: edge.target,
|
||
type: edge.type || 'post-tag',
|
||
weight: edge.strength || 1
|
||
}
|
||
});
|
||
} else {
|
||
console.warn(`Skipping edge e${index} due to missing node: ${edge.source} -> ${edge.target}`);
|
||
}
|
||
});
|
||
|
||
// Initialize Cytoscape
|
||
const cy = cytoscape({
|
||
container: graphContainer,
|
||
elements: elements,
|
||
style: [
|
||
// Base node styles
|
||
{ selector: 'node', style: {
|
||
'background-color': 'data(color)',
|
||
'label': 'data(label)',
|
||
'width': 'data(size)',
|
||
'height': 'data(size)',
|
||
'font-size': '10px',
|
||
'color': '#E2E8F0',
|
||
'text-valign': 'bottom',
|
||
'text-halign': 'center',
|
||
'text-margin-y': '7px',
|
||
'text-background-opacity': 0.7,
|
||
'text-background-color': '#0F1219',
|
||
'text-background-padding': '3px',
|
||
'text-background-shape': 'roundrectangle',
|
||
'text-max-width': '120px',
|
||
'text-wrap': 'ellipsis',
|
||
'text-overflow-wrap': 'anywhere',
|
||
'border-width': '2px',
|
||
'border-color': '#0F1219',
|
||
'border-opacity': 0.8,
|
||
'z-index': 10,
|
||
'text-outline-width': 1,
|
||
'text-outline-color': '#000',
|
||
'text-outline-opacity': 0.5
|
||
}},
|
||
// Post node specific styles
|
||
{ selector: 'node[type="post"]', style: {
|
||
'shape': 'ellipse',
|
||
'border-width': '2px'
|
||
}},
|
||
// Tag node specific styles
|
||
{ selector: 'node[type="tag"]', style: {
|
||
'shape': 'diamond',
|
||
'border-width': '1px',
|
||
'border-color': '#10B981',
|
||
'border-opacity': 0.9
|
||
}},
|
||
// Category node specific styles
|
||
{ selector: 'node[type="category"]', style: {
|
||
'shape': 'hexagon',
|
||
'border-width': '1px'
|
||
}},
|
||
// Edge styles
|
||
{ selector: 'edge', style: {
|
||
'width': 'mapData(weight, 1, 10, 1, 3)',
|
||
'line-color': 'rgba(226, 232, 240, 0.2)',
|
||
'curve-style': 'bezier',
|
||
'opacity': 0.6,
|
||
'z-index': 1
|
||
}},
|
||
// Post-tag edge specific styles
|
||
{ selector: 'edge[type="post-tag"]', style: {
|
||
'line-color': 'rgba(16, 185, 129, 0.4)',
|
||
'line-style': 'dashed'
|
||
}},
|
||
// Post-post edge specific styles
|
||
{ selector: 'edge[type="post-post"]', style: {
|
||
'line-color': 'rgba(59, 130, 246, 0.4)',
|
||
'line-style': 'solid'
|
||
}},
|
||
// Selection and hover styles
|
||
{ selector: '.highlighted', style: { 'background-color': 'data(color)', 'border-color': '#FFFFFF', 'border-width': '3px', 'color': '#FFFFFF', 'text-background-opacity': 0.9, 'opacity': 1, 'z-index': 20 } },
|
||
{ selector: '.filtered', style: { 'background-color': 'data(color)', 'border-color': '#FFFFFF', 'border-width': '2px', 'color': '#FFFFFF', 'text-background-opacity': 0.8, 'opacity': 0.8, 'z-index': 15 } },
|
||
{ selector: '.faded', style: { 'opacity': 0.15, 'text-opacity': 0.3, 'background-opacity': 0.3, 'z-index': 1 } },
|
||
{ selector: 'node:selected', style: { 'border-width': '4px', 'border-color': '#FFFFFF', 'border-opacity': 1, 'background-color': 'data(color)', 'text-opacity': 1, 'color': '#FFFFFF', 'z-index': 30 } },
|
||
{ selector: 'edge:selected', style: { 'width': 'mapData(weight, 1, 10, 2, 6)', 'line-color': '#FFFFFF', 'opacity': 1, 'z-index': 30 } }
|
||
],
|
||
// Update layout for better visualization of post-tag connections
|
||
layout: {
|
||
name: 'cose',
|
||
idealEdgeLength: 80,
|
||
nodeOverlap: 20,
|
||
refresh: 20,
|
||
fit: true,
|
||
padding: 30,
|
||
randomize: false,
|
||
componentSpacing: 100,
|
||
nodeRepulsion: 450000,
|
||
edgeElasticity: 100,
|
||
nestingFactor: 5,
|
||
gravity: 80,
|
||
numIter: 1000,
|
||
initialTemp: 200,
|
||
coolingFactor: 0.95,
|
||
minTemp: 1.0
|
||
},
|
||
zoom: 1, minZoom: 0.1, maxZoom: 3, zoomingEnabled: true, userZoomingEnabled: true, panningEnabled: true, userPanningEnabled: true, boxSelectionEnabled: false,
|
||
});
|
||
|
||
// Hide loading screen
|
||
if (loadingEl) {
|
||
setTimeout(() => { loadingEl.classList.add('hidden'); }, 500);
|
||
}
|
||
|
||
// --- Fullscreen Toggle Functionality ---
|
||
if (fullscreenToggle) {
|
||
fullscreenToggle.addEventListener('click', toggleFullscreen);
|
||
}
|
||
|
||
// Toggle fullscreen function
|
||
function toggleFullscreen() {
|
||
isFullscreen = !isFullscreen;
|
||
|
||
if (isFullscreen) {
|
||
// Enable fullscreen mode
|
||
graphWrapper.classList.add('fullscreen');
|
||
fullscreenEnterIcon.classList.add('hidden');
|
||
fullscreenExitIcon.classList.remove('hidden');
|
||
|
||
// Hide the node details panel if it's visible
|
||
if (nodeDetailsEl) nodeDetailsEl.classList.remove('active');
|
||
|
||
// In fullscreen, we adjust the cytoscape layout to fit
|
||
setTimeout(() => {
|
||
cy.resize();
|
||
cy.fit(null, 30);
|
||
}, 300); // Wait for transition to complete
|
||
} else {
|
||
// Disable fullscreen mode
|
||
graphWrapper.classList.remove('fullscreen');
|
||
fullscreenEnterIcon.classList.remove('hidden');
|
||
fullscreenExitIcon.classList.add('hidden');
|
||
|
||
// Hide the full post content panel
|
||
if (fullPostContent) {
|
||
fullPostContent.classList.remove('active');
|
||
}
|
||
|
||
// Reset the cytoscape layout
|
||
setTimeout(() => {
|
||
cy.resize();
|
||
cy.fit(null, 30);
|
||
}, 300); // Wait for transition to complete
|
||
}
|
||
}
|
||
|
||
// --- Interactions ---
|
||
let hoverTimeout;
|
||
cy.on('mouseover', 'node', function(e) {
|
||
const node = e.target;
|
||
clearTimeout(hoverTimeout);
|
||
node.addClass('highlighted');
|
||
node.connectedEdges().addClass('highlighted');
|
||
graphContainer.style.cursor = 'pointer';
|
||
});
|
||
|
||
cy.on('mouseout', 'node', function(e) {
|
||
const node = e.target;
|
||
hoverTimeout = setTimeout(() => {
|
||
if (!node.selected()) {
|
||
node.removeClass('highlighted');
|
||
node.connectedEdges().removeClass('highlighted');
|
||
}
|
||
}, 100);
|
||
graphContainer.style.cursor = 'default';
|
||
});
|
||
|
||
// Node click handler - dispatches to appropriate function based on fullscreen mode
|
||
cy.on('tap', 'node', function(e) {
|
||
const node = e.target;
|
||
|
||
if (isFullscreen) {
|
||
handleNodeClickFullscreen(node);
|
||
} else {
|
||
handleNodeClickNormal(node);
|
||
}
|
||
|
||
// Common highlight functionality for both modes
|
||
cy.elements().removeClass('highlighted').removeClass('faded');
|
||
node.addClass('highlighted');
|
||
node.neighborhood().addClass('highlighted');
|
||
cy.elements().difference(node.neighborhood().union(node)).addClass('faded');
|
||
});
|
||
|
||
// Handle node click in normal mode - shows metadata in sidebar
|
||
function handleNodeClickNormal(node) {
|
||
const nodeData = node.data();
|
||
|
||
if (nodeDetailsEl) {
|
||
// Set node title
|
||
document.getElementById('node-title').textContent = nodeData.label;
|
||
|
||
// Set node type
|
||
const nodeTypeEl = document.getElementById('node-type').querySelector('.type-value') ||
|
||
document.getElementById('node-type');
|
||
nodeTypeEl.textContent = nodeData.type.charAt(0).toUpperCase() + nodeData.type.slice(1);
|
||
nodeTypeEl.className = `type-value ${nodeData.type}-type`;
|
||
|
||
// Set category if it's a post
|
||
const categorySection = document.getElementById('node-category');
|
||
if (nodeData.type === 'post' && nodeData.category) {
|
||
categorySection.style.display = 'block';
|
||
const categoryEl = categorySection.querySelector('.category-value');
|
||
categoryEl.textContent = nodeData.category;
|
||
// Use category colors
|
||
const catColor = predefinedColors[nodeData.category] || 'var(--text-secondary)';
|
||
categoryEl.style.backgroundColor = `${catColor}33`; // Add alpha
|
||
categoryEl.style.color = catColor;
|
||
} else {
|
||
categorySection.style.display = 'none';
|
||
}
|
||
|
||
// Set tags if it's a post
|
||
const tagsSection = document.getElementById('node-tags');
|
||
const tagsContainer = tagsSection.querySelector('.tags-container');
|
||
|
||
if (nodeData.type === 'post' && nodeData.tags && nodeData.tags.length > 0) {
|
||
tagsSection.style.display = 'block';
|
||
tagsContainer.innerHTML = '';
|
||
nodeData.tags.forEach(tag => {
|
||
const tagEl = document.createElement('span');
|
||
tagEl.className = 'tag';
|
||
tagEl.textContent = tag;
|
||
tagEl.addEventListener('click', () => {
|
||
// Find the tag node and trigger its selection
|
||
const tagNode = cy.getElementById(`tag-${tag}`);
|
||
if (tagNode.length > 0) {
|
||
tagNode.trigger('tap');
|
||
}
|
||
|
||
// Try to find and click tag filter button
|
||
try {
|
||
const tagFilterBtn = Array.from(
|
||
document.querySelectorAll('.tag-filter-btn')
|
||
).find(btn => btn.dataset.tag === tag);
|
||
|
||
if (tagFilterBtn) {
|
||
tagFilterBtn.click();
|
||
}
|
||
} catch (e) {
|
||
console.log('Tag filter button not found');
|
||
}
|
||
});
|
||
tagsContainer.appendChild(tagEl);
|
||
});
|
||
} else {
|
||
tagsSection.style.display = 'none';
|
||
}
|
||
|
||
// Set connections
|
||
const connectionsList = document.getElementById('node-connections').querySelector('.connections-list');
|
||
connectionsList.innerHTML = '';
|
||
const connectedNodes = node.neighborhood('node');
|
||
if (connectedNodes.length > 0) {
|
||
connectedNodes.forEach(connectedNode => {
|
||
const connectedData = connectedNode.data();
|
||
const listItem = document.createElement('li');
|
||
const link = document.createElement('a');
|
||
link.href = '#';
|
||
link.textContent = connectedData.label;
|
||
link.dataset.id = connectedData.id;
|
||
link.addEventListener('click', (evt) => {
|
||
evt.preventDefault();
|
||
cy.$(':selected').unselect();
|
||
const targetNode = cy.getElementById(connectedData.id);
|
||
if (targetNode) {
|
||
targetNode.select();
|
||
cy.animate({ center: { eles: targetNode }, zoom: cy.zoom() }, { duration: 300 });
|
||
targetNode.trigger('tap');
|
||
}
|
||
});
|
||
listItem.appendChild(link);
|
||
connectionsList.appendChild(listItem);
|
||
});
|
||
} else {
|
||
connectionsList.innerHTML = '<li>No connections</li>';
|
||
}
|
||
|
||
// Set link text and URL based on node type
|
||
const nodeLink = document.getElementById('node-link');
|
||
if (nodeData.type === 'post') {
|
||
nodeLink.textContent = 'Read Article';
|
||
} else if (nodeData.type === 'tag') {
|
||
nodeLink.textContent = 'Browse Tag';
|
||
} else {
|
||
nodeLink.textContent = 'View Content';
|
||
}
|
||
nodeLink.href = nodeData.url;
|
||
|
||
nodeDetailsEl.classList.add('active');
|
||
}
|
||
|
||
// If it's a tag node, try to trigger the corresponding tag filter
|
||
if (nodeData.type === 'tag') {
|
||
const tagName = nodeData.label.replace(/^#/, ''); // Remove # prefix if present
|
||
try {
|
||
// Find and click the corresponding tag filter button
|
||
const tagFilterBtn = Array.from(
|
||
document.querySelectorAll('.tag-filter-btn')
|
||
).find(btn => btn.dataset.tag === tagName);
|
||
|
||
if (tagFilterBtn) {
|
||
tagFilterBtn.click();
|
||
}
|
||
|
||
// Scroll to the blog section
|
||
const blogSection = document.querySelector('.blog-posts-section');
|
||
if (blogSection) {
|
||
blogSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
}
|
||
} catch (e) {
|
||
console.log('Tag filter button not found or blog section not found');
|
||
}
|
||
}
|
||
}
|
||
|
||
// Handle node click in fullscreen mode - shows full content in side panel
|
||
function handleNodeClickFullscreen(node) {
|
||
const nodeData = node.data();
|
||
console.log('Node clicked in fullscreen mode:', nodeData); // Debug log
|
||
|
||
// Only show content for post nodes
|
||
if (nodeData.type === 'post') {
|
||
if (fullPostContent) {
|
||
// Set post title
|
||
fullPostTitle.textContent = nodeData.label;
|
||
|
||
// Set post category if available
|
||
if (nodeData.category) {
|
||
const catColor = predefinedColors[nodeData.category] || 'var(--accent-primary)';
|
||
fullPostCategory.textContent = nodeData.category;
|
||
fullPostCategory.style.backgroundColor = `${catColor}33`;
|
||
fullPostCategory.style.color = catColor;
|
||
fullPostCategory.parentElement.style.display = 'flex';
|
||
} else {
|
||
fullPostCategory.parentElement.style.display = 'none';
|
||
}
|
||
|
||
// Set post tags if available
|
||
if (nodeData.tags && nodeData.tags.length > 0) {
|
||
fullPostTags.innerHTML = '';
|
||
nodeData.tags.forEach(tag => {
|
||
const tagEl = document.createElement('span');
|
||
tagEl.className = 'post-tag';
|
||
tagEl.textContent = tag;
|
||
fullPostTags.appendChild(tagEl);
|
||
});
|
||
fullPostTags.parentElement.style.display = 'flex';
|
||
} else {
|
||
fullPostTags.parentElement.style.display = 'none';
|
||
}
|
||
|
||
// Check if content is available
|
||
console.log('Content available:', !!nodeData.content); // Debug log
|
||
|
||
// Display the content or a placeholder
|
||
if (nodeData.content) {
|
||
fullPostContainer.innerHTML = `
|
||
<article class="post-content">
|
||
${nodeData.content}
|
||
</article>
|
||
`;
|
||
} else {
|
||
// Try to fetch the content if not available
|
||
console.log('Attempting to fetch content from:', nodeData.url);
|
||
fullPostContainer.innerHTML = `
|
||
<div class="loading-content">
|
||
<div class="spinner"></div>
|
||
<p>Loading content...</p>
|
||
</div>
|
||
`;
|
||
|
||
// Try to load the content from the post URL
|
||
fetch(nodeData.url)
|
||
.then(response => response.text())
|
||
.then(html => {
|
||
// Extract the post content
|
||
const parser = new DOMParser();
|
||
const doc = parser.parseFromString(html, 'text/html');
|
||
const articleContent = doc.querySelector('article') ||
|
||
doc.querySelector('.content') ||
|
||
doc.querySelector('main');
|
||
|
||
if (articleContent) {
|
||
fullPostContainer.innerHTML = `
|
||
<article class="post-content">
|
||
${articleContent.innerHTML}
|
||
</article>
|
||
`;
|
||
} else {
|
||
// If we can't extract content, show the placeholder
|
||
showContentPlaceholder("Could not load content. Click 'Read Full Article' to view on the blog.");
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error fetching content:', error);
|
||
showContentPlaceholder("Error loading content. Click 'Read Full Article' to view on the blog.");
|
||
});
|
||
}
|
||
|
||
// Set the article link
|
||
fullPostLink.href = nodeData.url || '#';
|
||
|
||
// Make the content panel visible
|
||
fullPostContent.classList.add('active');
|
||
fullPostContent.style.display = 'flex';
|
||
}
|
||
} else {
|
||
// Hide the content panel for non-post nodes
|
||
if (fullPostContent) {
|
||
fullPostContent.classList.remove('active');
|
||
fullPostContent.style.display = 'none';
|
||
}
|
||
}
|
||
}
|
||
|
||
// Helper function to show content placeholder
|
||
function showContentPlaceholder() {
|
||
fullPostContainer.innerHTML = `
|
||
<div class="full-post-placeholder">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||
<polyline points="14 2 14 8 20 8"></polyline>
|
||
<line x1="16" y1="13" x2="8" y2="13"></line>
|
||
<line x1="16" y1="17" x2="8" y2="17"></line>
|
||
<polyline points="10 9 9 9 8 9"></polyline>
|
||
</svg>
|
||
<p>Full content preview not available.</p>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Helper function to fetch post content if not provided
|
||
// async function fetchPostContent(url) {
|
||
// if (!url || url === '#') return null;
|
||
// try {
|
||
// const response = await fetch(url);
|
||
// if (!response.ok) return null;
|
||
// const html = await response.text();
|
||
// const parser = new DOMParser();
|
||
// const doc = parser.parseFromString(html, 'text/html');
|
||
// const articleContent = doc.querySelector('article') || doc.querySelector('.content') || doc.querySelector('main');
|
||
// return articleContent ? articleContent.innerHTML : null;
|
||
// } catch (error) {
|
||
// console.error('Error fetching post content:', error);
|
||
// return null;
|
||
// }
|
||
// }
|
||
|
||
// Modify background click handler to account for fullscreen mode
|
||
cy.on('tap', function(e) {
|
||
if (e.target === cy) {
|
||
if (nodeDetailsEl) nodeDetailsEl.classList.remove('active');
|
||
if (isFullscreen && fullPostContent) {
|
||
fullPostContent.classList.remove('active');
|
||
}
|
||
cy.elements().removeClass('selected highlighted faded');
|
||
}
|
||
});
|
||
|
||
if (closeDetailsBtn) {
|
||
closeDetailsBtn.addEventListener('click', () => {
|
||
if (nodeDetailsEl) nodeDetailsEl.classList.remove('active');
|
||
cy.$(':selected').unselect();
|
||
cy.elements().removeClass('highlighted faded');
|
||
});
|
||
}
|
||
|
||
// Close full post panel button
|
||
if (closeFullPostBtn) {
|
||
closeFullPostBtn.addEventListener('click', () => {
|
||
if (fullPostContent) {
|
||
fullPostContent.classList.remove('active');
|
||
}
|
||
cy.$(':selected').unselect();
|
||
cy.elements().removeClass('highlighted faded');
|
||
});
|
||
}
|
||
|
||
// Filter graph by node type
|
||
const filterButtons = document.querySelectorAll('.graph-filter');
|
||
filterButtons.forEach(button => {
|
||
button.addEventListener('click', () => {
|
||
filterButtons.forEach(btn => btn.classList.remove('active'));
|
||
button.classList.add('active');
|
||
const filter = button.dataset.filter;
|
||
|
||
if (filter === 'all') {
|
||
cy.elements().removeClass('faded highlighted filtered');
|
||
} else if (filter === 'posts') {
|
||
cy.elements().addClass('faded').removeClass('highlighted filtered');
|
||
const postNodes = cy.nodes().filter(node => node.data('type') === 'post');
|
||
postNodes.removeClass('faded').addClass('filtered');
|
||
} else if (filter === 'tags') {
|
||
cy.elements().addClass('faded').removeClass('highlighted filtered');
|
||
const tagNodes = cy.nodes().filter(node => node.data('type') === 'tag');
|
||
tagNodes.removeClass('faded').addClass('filtered');
|
||
}
|
||
});
|
||
});
|
||
|
||
// Zoom controls
|
||
document.getElementById('zoom-in')?.addEventListener('click', () => cy.zoom(cy.zoom() * 1.2));
|
||
document.getElementById('zoom-out')?.addEventListener('click', () => cy.zoom(cy.zoom() / 1.2));
|
||
document.getElementById('reset-graph')?.addEventListener('click', () => {
|
||
cy.fit(null, 30);
|
||
cy.elements().removeClass('faded highlighted filtered');
|
||
const allFilterButton = document.querySelector('.graph-filter[data-filter="all"]');
|
||
if (allFilterButton) allFilterButton.click();
|
||
});
|
||
|
||
// Add mouse wheel zoom controls
|
||
cy.on('zoom', function() {
|
||
if (cy.zoom() > 1.5) {
|
||
cy.style().selector('node').style({ 'text-max-width': '150px', 'font-size': '12px' }).update();
|
||
} else {
|
||
cy.style().selector('node').style({ 'text-max-width': '120px', 'font-size': '10px' }).update();
|
||
}
|
||
});
|
||
|
||
// Apply initial filter if specified
|
||
if (initialFilter && initialFilter !== 'all') {
|
||
const filterButton = document.querySelector(`.graph-filter[data-filter="${initialFilter}"]`);
|
||
if (filterButton) {
|
||
setTimeout(() => filterButton.click(), 500);
|
||
}
|
||
}
|
||
|
||
// Connect search input if it exists
|
||
const searchInput = document.getElementById('search-input');
|
||
if (searchInput) {
|
||
searchInput.addEventListener('input', e => {
|
||
const term = e.target.value.toLowerCase();
|
||
|
||
if (!term) {
|
||
// Reset graph view if search is cleared
|
||
cy.elements().removeClass('highlighted faded filtered');
|
||
const allFilterBtn = document.querySelector('.graph-filter[data-filter="all"]');
|
||
if (allFilterBtn) allFilterBtn.click();
|
||
return;
|
||
}
|
||
|
||
// Reset previous filters
|
||
cy.elements().addClass('faded').removeClass('highlighted filtered');
|
||
|
||
// Find nodes that match the search term
|
||
const matchingNodes = cy.nodes().filter(node => {
|
||
const label = node.data('label').toLowerCase();
|
||
return label.includes(term);
|
||
});
|
||
|
||
if (matchingNodes.length) {
|
||
matchingNodes.removeClass('faded').addClass('highlighted');
|
||
matchingNodes.connectedEdges().removeClass('faded');
|
||
}
|
||
});
|
||
}
|
||
|
||
// Connect tag filter buttons if they exist
|
||
const tagFilterButtons = document.querySelectorAll('.tag-filter-btn');
|
||
if (tagFilterButtons.length) {
|
||
tagFilterButtons.forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const tagName = btn.dataset.tag;
|
||
|
||
if (tagName === 'all') {
|
||
// Reset graph view if "All" is selected
|
||
cy.elements().removeClass('highlighted faded filtered');
|
||
const allFilterBtn = document.querySelector('.graph-filter[data-filter="all"]');
|
||
// Add event listener to prevent redirect in fullscreen mode
|
||
if (fullPostLink) {
|
||
fullPostLink.addEventListener('click', (e) => {
|
||
if (isFullscreen) {
|
||
// If in fullscreen, prevent default behavior to keep the user in the graph view
|
||
e.preventDefault();
|
||
|
||
// Instead, display a message to exit fullscreen to visit the full article
|
||
const message = document.createElement('div');
|
||
message.className = 'fullscreen-message';
|
||
message.textContent = 'Exit fullscreen to visit the full article page';
|
||
message.style.position = 'absolute';
|
||
message.style.bottom = '70px'; // Adjust as needed
|
||
message.style.left = '50%';
|
||
message.style.transform = 'translateX(-50%)';
|
||
message.style.background = 'rgba(0, 0, 0, 0.75)';
|
||
message.style.color = 'white';
|
||
message.style.padding = '8px 16px';
|
||
message.style.borderRadius = '4px';
|
||
message.style.zIndex = '1000';
|
||
message.style.transition = 'opacity 0.3s ease';
|
||
|
||
fullPostContent.appendChild(message);
|
||
|
||
// Remove the message after 3 seconds
|
||
setTimeout(() => {
|
||
message.style.opacity = '0';
|
||
setTimeout(() => {
|
||
message.remove();
|
||
}, 300);
|
||
}, 3000);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Listen for ESC key to exit fullscreen
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape' && isFullscreen) {
|
||
toggleFullscreen();
|
||
}
|
||
});
|
||
|
||
// Update window resize handler to preserve layout in fullscreen mode
|
||
window.addEventListener('resize', () => {
|
||
if (cy) {
|
||
cy.resize();
|
||
if (!isFullscreen) {
|
||
cy.fit(null, 30);
|
||
}
|
||
}
|
||
});
|
||
if (allFilterBtn) allFilterBtn.click();
|
||
return;
|
||
}
|
||
|
||
// Find the tag node
|
||
const tagNode = cy.nodes().filter(node =>
|
||
node.data('type') === 'tag' &&
|
||
node.data('label') === tagName
|
||
);
|
||
|
||
if (tagNode.length) {
|
||
// Highlight this tag and connected posts in the graph
|
||
cy.elements().addClass('faded').removeClass('highlighted filtered');
|
||
tagNode.removeClass('faded').addClass('highlighted');
|
||
|
||
// Get connected posts
|
||
const connectedPosts = tagNode.neighborhood('node[type="post"]');
|
||
connectedPosts.removeClass('faded').addClass('filtered');
|
||
|
||
// Highlight connecting edges
|
||
tagNode.connectedEdges().removeClass('faded').addClass('highlighted');
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// Dispatch graphReady event for external listeners
|
||
document.dispatchEvent(new CustomEvent('graphReady', { detail: { cy } }));
|
||
|
||
// Add event listener to prevent redirect in fullscreen mode
|
||
if (fullPostLink) {
|
||
fullPostLink.addEventListener('click', (e) => {
|
||
if (isFullscreen) {
|
||
// If in fullscreen, prevent default behavior to keep the user in the graph view
|
||
e.preventDefault();
|
||
|
||
// Instead, display a message to exit fullscreen to visit the full article
|
||
const message = document.createElement('div');
|
||
message.className = 'fullscreen-message';
|
||
message.textContent = 'Exit fullscreen to visit the full article page';
|
||
message.style.position = 'absolute';
|
||
message.style.bottom = '70px'; // Adjust as needed
|
||
message.style.left = '50%';
|
||
message.style.transform = 'translateX(-50%)';
|
||
message.style.background = 'rgba(0, 0, 0, 0.75)';
|
||
message.style.color = 'white';
|
||
message.style.padding = '8px 16px';
|
||
message.style.borderRadius = '4px';
|
||
message.style.zIndex = '1000';
|
||
message.style.transition = 'opacity 0.3s ease';
|
||
|
||
fullPostContent.appendChild(message);
|
||
|
||
// Remove the message after 3 seconds
|
||
setTimeout(() => {
|
||
message.style.opacity = '0';
|
||
setTimeout(() => {
|
||
message.remove();
|
||
}, 300);
|
||
}, 3000);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Listen for ESC key to exit fullscreen
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape' && isFullscreen) {
|
||
toggleFullscreen();
|
||
}
|
||
});
|
||
|
||
// Update window resize handler to preserve layout in fullscreen mode
|
||
window.addEventListener('resize', () => {
|
||
if (cy) {
|
||
cy.resize();
|
||
if (!isFullscreen) {
|
||
cy.fit(null, 30);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Initialize graph on DOMContentLoaded or if already loaded
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', initializeGraph);
|
||
} else {
|
||
initializeGraph();
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
/* Updated Graph Container Styles */
|
||
.graph-container-wrapper {
|
||
position: relative;
|
||
width: 100%;
|
||
height: var(--graph-height, 50vh);
|
||
min-height: 400px;
|
||
max-height: 800px;
|
||
margin-bottom: 2rem;
|
||
transition: all 0.5s cubic-bezier(0.19, 1, 0.22, 1);
|
||
}
|
||
|
||
/* Improved Fullscreen mode styles */
|
||
.graph-container-wrapper.fullscreen {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100vw;
|
||
height: 100vh;
|
||
max-height: 100vh;
|
||
z-index: 9999;
|
||
margin: 0;
|
||
padding: 0;
|
||
background: var(--bg-primary);
|
||
backdrop-filter: blur(10px);
|
||
display: flex;
|
||
border-radius: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.graph-container {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
border: 1px solid var(--card-border);
|
||
background: rgba(15, 23, 42, 0.2);
|
||
backdrop-filter: blur(5px);
|
||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
/* Adjust graph container in fullscreen mode */
|
||
.graph-container-wrapper.fullscreen .graph-container {
|
||
position: relative;
|
||
width: 60%;
|
||
height: 100%;
|
||
border-radius: 0;
|
||
margin: 0;
|
||
border: none;
|
||
border-right: 1px solid var(--border-primary);
|
||
}
|
||
|
||
#knowledge-graph {
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: 1;
|
||
}
|
||
|
||
/* Overlay instructions - position over the graph instead of taking up space */
|
||
.graph-instructions.overlay {
|
||
position: absolute;
|
||
top: 20px;
|
||
left: 20px;
|
||
width: auto;
|
||
max-width: 350px;
|
||
z-index: 5;
|
||
margin: 0;
|
||
}
|
||
|
||
.graph-container-wrapper.fullscreen .graph-instructions.overlay {
|
||
top: 80px;
|
||
}
|
||
|
||
.instructions-details {
|
||
background: rgba(15, 23, 42, 0.75);
|
||
border-radius: 8px;
|
||
border: 1px solid var(--border-primary);
|
||
overflow: hidden;
|
||
transition: all 0.3s ease;
|
||
backdrop-filter: blur(5px);
|
||
}
|
||
|
||
.instructions-details[open] {
|
||
padding-bottom: 1rem;
|
||
}
|
||
|
||
.instructions-summary {
|
||
padding: 0.75rem 1rem;
|
||
cursor: pointer;
|
||
color: var(--text-primary);
|
||
font-weight: 500;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.instructions-summary::before {
|
||
content: "ℹ️";
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.instructions-content {
|
||
padding: 0 1rem;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.instructions-content ul {
|
||
padding-left: 1.5rem;
|
||
margin: 0.5rem 0 1rem;
|
||
}
|
||
|
||
.node-example {
|
||
display: inline-block;
|
||
width: 12px;
|
||
height: 12px;
|
||
margin-right: 0.25rem;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.post-node {
|
||
background-color: #3B82F6;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.tag-node {
|
||
background-color: #10B981;
|
||
transform: rotate(45deg);
|
||
}
|
||
|
||
/* Fullscreen Toggle Button */
|
||
.fullscreen-toggle {
|
||
position: absolute;
|
||
top: 20px;
|
||
right: 20px;
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 8px;
|
||
background: rgba(30, 41, 59, 0.7);
|
||
border: 1px solid var(--border-primary);
|
||
color: var(--text-primary);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
z-index: 6;
|
||
transition: all 0.2s ease;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.fullscreen-toggle:hover {
|
||
background: rgba(30, 41, 59, 0.9);
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.graph-container-wrapper.fullscreen .fullscreen-toggle {
|
||
top: 20px;
|
||
right: 20px;
|
||
}
|
||
|
||
/* Hide one of the fullscreen icons */
|
||
.hidden {
|
||
display: none;
|
||
}
|
||
|
||
/* Full Post Content Panel - Improved styling */
|
||
.full-post-content {
|
||
position: absolute;
|
||
top: 0;
|
||
right: 0;
|
||
width: 0;
|
||
height: 100%;
|
||
overflow: hidden;
|
||
transition: all 0.3s cubic-bezier(0.19, 1, 0.22, 1);
|
||
background: var(--bg-secondary);
|
||
border-radius: 12px;
|
||
opacity: 0;
|
||
z-index: 4;
|
||
border: 1px solid var(--card-border);
|
||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||
visibility: hidden;
|
||
display: none;
|
||
}
|
||
|
||
/* Full post panel in fullscreen mode */
|
||
.graph-container-wrapper.fullscreen .full-post-content {
|
||
position: relative;
|
||
width: 40%;
|
||
height: 100%;
|
||
background: rgba(15, 23, 42, 0.95);
|
||
backdrop-filter: blur(10px);
|
||
display: flex;
|
||
flex-direction: column;
|
||
opacity: 1;
|
||
visibility: visible;
|
||
border: none;
|
||
border-radius: 0;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.graph-container-wrapper.fullscreen .full-post-content.active {
|
||
display: flex;
|
||
}
|
||
|
||
.full-post-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 1.5rem;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||
background: rgba(15, 23, 42, 0.8);
|
||
}
|
||
|
||
.full-post-title {
|
||
font-size: 1.5rem;
|
||
margin: 0;
|
||
color: var(--text-primary);
|
||
font-weight: 600;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.full-post-metadata {
|
||
padding: 1rem 1.5rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.75rem;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||
background: rgba(15, 23, 42, 0.6);
|
||
}
|
||
|
||
.metadata-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.metadata-label {
|
||
color: var(--text-secondary);
|
||
font-size: 0.85rem;
|
||
font-family: var(--font-mono);
|
||
}
|
||
|
||
.full-post-category {
|
||
display: inline-block;
|
||
padding: 0.25rem 0.75rem;
|
||
border-radius: 20px;
|
||
font-size: 0.85rem;
|
||
font-weight: 500;
|
||
font-family: var(--font-mono);
|
||
}
|
||
|
||
.full-post-tags {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.post-tag {
|
||
background: rgba(16, 185, 129, 0.1);
|
||
color: #10B981;
|
||
padding: 0.25rem 0.5rem;
|
||
border-radius: 4px;
|
||
font-size: 0.75rem;
|
||
font-family: var(--font-mono);
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.post-tag:hover {
|
||
background: rgba(16, 185, 129, 0.2);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.full-post-container {
|
||
flex: 1;
|
||
padding: 1.5rem;
|
||
overflow-y: auto;
|
||
color: var(--text-primary);
|
||
background: var(--bg-secondary);
|
||
}
|
||
|
||
.full-post-container::-webkit-scrollbar {
|
||
width: 8px;
|
||
}
|
||
|
||
.full-post-container::-webkit-scrollbar-track {
|
||
background: rgba(30, 41, 59, 0.5);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.full-post-container::-webkit-scrollbar-thumb {
|
||
background: rgba(226, 232, 240, 0.1);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.full-post-container::-webkit-scrollbar-thumb:hover {
|
||
background: rgba(226, 232, 240, 0.2);
|
||
}
|
||
|
||
.full-post-placeholder {
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--text-secondary);
|
||
text-align: center;
|
||
padding: 2rem;
|
||
}
|
||
|
||
.full-post-placeholder svg {
|
||
margin-bottom: 1rem;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.post-content {
|
||
line-height: 1.7;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.post-content h1, .post-content h2, .post-content h3,
|
||
.post-content h4, .post-content h5, .post-content h6 {
|
||
margin-top: 1.5rem;
|
||
margin-bottom: 1rem;
|
||
color: var(--text-primary);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.post-content p {
|
||
margin-bottom: 1.25rem;
|
||
}
|
||
|
||
.post-content a {
|
||
color: var(--accent-primary);
|
||
text-decoration: none;
|
||
border-bottom: 1px dotted var(--accent-primary);
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.post-content a:hover {
|
||
color: var(--accent-secondary);
|
||
border-bottom-style: solid;
|
||
}
|
||
|
||
.post-content pre {
|
||
background: rgba(15, 23, 42, 0.6);
|
||
padding: 1rem;
|
||
border-radius: 6px;
|
||
overflow-x: auto;
|
||
margin: 1.5rem 0;
|
||
border: 1px solid var(--border-primary);
|
||
}
|
||
|
||
.post-content code {
|
||
font-family: var(--font-mono);
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.post-content img {
|
||
max-width: 100%;
|
||
height: auto;
|
||
margin: 1.5rem 0;
|
||
border-radius: 6px;
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.post-content blockquote {
|
||
border-left: 4px solid var(--accent-primary);
|
||
padding-left: 1rem;
|
||
margin-left: 0;
|
||
margin-right: 0;
|
||
font-style: italic;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.post-content ul, .post-content ol {
|
||
padding-left: 1.5rem;
|
||
margin-bottom: 1.25rem;
|
||
}
|
||
|
||
.post-content li {
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.full-post-footer {
|
||
padding: 1rem;
|
||
background: rgba(15, 23, 42, 0.7);
|
||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
.full-post-link {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 0.5rem;
|
||
padding: 1rem;
|
||
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
|
||
color: var(--bg-primary);
|
||
text-decoration: none;
|
||
font-weight: 500;
|
||
transition: all 0.3s ease;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.full-post-link:hover {
|
||
background: linear-gradient(90deg, var(--accent-secondary), var(--accent-primary));
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(6, 182, 212, 0.3);
|
||
}
|
||
|
||
/* Media Queries for Fullscreen */
|
||
@media screen and (max-width: 992px) {
|
||
/* Adjust fullscreen layout for smaller screens */
|
||
.graph-container-wrapper.fullscreen {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.graph-container-wrapper.fullscreen .graph-container {
|
||
width: 100%;
|
||
height: 50%;
|
||
margin-right: 0;
|
||
}
|
||
|
||
.graph-container-wrapper.fullscreen .full-post-content {
|
||
width: 100%;
|
||
height: 50%;
|
||
}
|
||
}
|
||
|
||
@media screen and (max-width: 768px) {
|
||
.node-details {
|
||
width: 85%;
|
||
max-width: 300px;
|
||
left: 50%;
|
||
right: auto;
|
||
transform: translate(-50%, 120%);
|
||
bottom: 20px;
|
||
top: auto;
|
||
}
|
||
|
||
.node-details.active {
|
||
transform: translate(-50%, 0);
|
||
}
|
||
|
||
.graph-instructions.overlay {
|
||
top: 15px;
|
||
left: 15px;
|
||
max-width: 300px;
|
||
}
|
||
|
||
.graph-controls {
|
||
bottom: 10px;
|
||
}
|
||
|
||
.graph-filters {
|
||
padding: 0.5rem;
|
||
}
|
||
|
||
.graph-filter {
|
||
padding: 0.3rem 0.6rem;
|
||
font-size: 0.75rem;
|
||
}
|
||
|
||
.fullscreen-toggle {
|
||
top: 15px;
|
||
right: 15px;
|
||
width: 36px;
|
||
height: 36px;
|
||
}
|
||
|
||
.graph-container-wrapper.fullscreen .graph-container {
|
||
height: 40%;
|
||
}
|
||
|
||
.graph-container-wrapper.fullscreen .full-post-content {
|
||
height: 60%;
|
||
}
|
||
|
||
.full-post-header {
|
||
padding: 1rem;
|
||
}
|
||
|
||
.full-post-title {
|
||
font-size: 1.25rem;
|
||
}
|
||
|
||
.full-post-metadata {
|
||
padding: 0.75rem 1rem;
|
||
}
|
||
|
||
.full-post-container {
|
||
padding: 1rem;
|
||
}
|
||
}
|
||
|
||
@media screen and (max-width: 480px) {
|
||
.graph-container-wrapper.fullscreen {
|
||
padding: 0;
|
||
}
|
||
|
||
.graph-container-wrapper.fullscreen .graph-container {
|
||
height: 35%;
|
||
}
|
||
|
||
.graph-container-wrapper.fullscreen .full-post-content {
|
||
height: 65%;
|
||
}
|
||
|
||
.full-post-title {
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
.graph-instructions.overlay {
|
||
max-width: 250px;
|
||
}
|
||
}
|
||
|
||
/* Graph Container Styles */
|
||
/* .graph-container-wrapper is defined earlier */
|
||
/* .graph-container is defined earlier */
|
||
/* #knowledge-graph is defined earlier */
|
||
|
||
/* Loading Animation */
|
||
.graph-loading {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(15, 23, 42, 0.7);
|
||
z-index: 10;
|
||
transition: opacity 0.5s ease, visibility 0.5s ease;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.graph-loading.hidden {
|
||
opacity: 0;
|
||
visibility: hidden;
|
||
}
|
||
|
||
.loading-spinner {
|
||
position: relative;
|
||
width: 80px;
|
||
height: 80px;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.spinner-ring {
|
||
position: absolute;
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 50%;
|
||
border: 3px solid transparent;
|
||
border-top-color: var(--accent-primary);
|
||
animation: spin 1.5s linear infinite;
|
||
}
|
||
|
||
.spinner-ring:nth-child(2) {
|
||
width: calc(100% - 15px);
|
||
height: calc(100% - 15px);
|
||
top: 7.5px;
|
||
left: 7.5px;
|
||
border-top-color: var(--accent-secondary);
|
||
animation-duration: 2s;
|
||
animation-direction: reverse;
|
||
}
|
||
|
||
.spinner-ring:nth-child(3) {
|
||
width: calc(100% - 30px);
|
||
height: calc(100% - 30px);
|
||
top: 15px;
|
||
left: 15px;
|
||
border-top-color: var(--accent-tertiary);
|
||
animation-duration: 2.5s;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
.loading-text {
|
||
color: var(--text-primary);
|
||
font-family: var(--font-mono);
|
||
font-size: 1rem;
|
||
letter-spacing: 0.05em;
|
||
}
|
||
|
||
/* Node Details Panel */
|
||
.node-details {
|
||
position: absolute;
|
||
top: 20px;
|
||
right: 20px;
|
||
width: 300px;
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--card-border);
|
||
border-radius: 10px;
|
||
padding: 1.5rem;
|
||
z-index: 5;
|
||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
||
transform: translateX(120%);
|
||
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||
opacity: 0;
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
|
||
.node-details.active {
|
||
transform: translateX(0);
|
||
opacity: 1;
|
||
}
|
||
|
||
.node-details-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.node-title {
|
||
font-size: 1.2rem;
|
||
margin: 0;
|
||
color: var(--text-primary);
|
||
font-weight: 600;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.close-button {
|
||
background: none;
|
||
border: none;
|
||
color: var(--text-secondary);
|
||
cursor: pointer;
|
||
padding: 5px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.close-button:hover {
|
||
color: var(--text-primary);
|
||
background: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.node-type {
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.type-value {
|
||
display: inline-block;
|
||
padding: 0.25rem 0.75rem;
|
||
border-radius: 20px;
|
||
font-size: 0.85rem;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.post-type {
|
||
background-color: rgba(59, 130, 246, 0.15);
|
||
color: #3B82F6;
|
||
}
|
||
|
||
.tag-type {
|
||
background-color: rgba(16, 185, 129, 0.15);
|
||
color: #10B981;
|
||
}
|
||
|
||
.category-type {
|
||
background-color: rgba(139, 92, 246, 0.15);
|
||
color: #8B5CF6;
|
||
}
|
||
|
||
.node-category, .node-tags, .node-connections {
|
||
margin-bottom: 1.25rem;
|
||
}
|
||
|
||
.category-label, .tags-label, .connections-label {
|
||
display: block;
|
||
color: var(--text-secondary);
|
||
font-size: 0.85rem;
|
||
margin-bottom: 0.5rem;
|
||
font-family: var(--font-mono);
|
||
}
|
||
|
||
.category-value {
|
||
display: inline-block;
|
||
padding: 0.25rem 0.75rem;
|
||
border-radius: 20px;
|
||
font-size: 0.85rem;
|
||
font-family: var(--font-mono);
|
||
}
|
||
|
||
.tags-container {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.tag {
|
||
background: rgba(16, 185, 129, 0.1);
|
||
color: #10B981;
|
||
padding: 0.25rem 0.5rem;
|
||
border-radius: 4px;
|
||
font-size: 0.75rem;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.tag:hover {
|
||
background: rgba(16, 185, 129, 0.2);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.no-tags {
|
||
color: var(--text-tertiary);
|
||
font-style: italic;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.connections-list {
|
||
padding-left: 0;
|
||
list-style: none;
|
||
margin: 0;
|
||
max-height: 150px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.connections-list li {
|
||
color: var(--text-secondary);
|
||
font-size: 0.9rem;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.connections-list a {
|
||
color: var(--accent-primary);
|
||
text-decoration: none;
|
||
transition: color 0.2s ease;
|
||
}
|
||
|
||
.connections-list a:hover {
|
||
color: var(--accent-secondary);
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.node-link {
|
||
display: block;
|
||
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
|
||
color: var(--bg-primary);
|
||
font-weight: 500;
|
||
padding: 0.6rem 1.25rem;
|
||
border-radius: 6px;
|
||
text-decoration: none;
|
||
text-align: center;
|
||
transition: all 0.3s ease;
|
||
box-shadow: 0 4px 10px rgba(6, 182, 212, 0.2);
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
.node-link:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 15px rgba(6, 182, 212, 0.3);
|
||
}
|
||
|
||
/* Graph Controls */
|
||
.graph-controls {
|
||
position: absolute;
|
||
bottom: 20px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
z-index: 5;
|
||
}
|
||
|
||
.graph-filters {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.5rem;
|
||
justify-content: center;
|
||
padding: 0.75rem;
|
||
background: rgba(15, 23, 42, 0.7);
|
||
backdrop-filter: blur(10px);
|
||
border-radius: 30px;
|
||
border: 1px solid var(--border-primary);
|
||
}
|
||
|
||
.graph-filter {
|
||
background: rgba(226, 232, 240, 0.05);
|
||
color: var(--text-secondary);
|
||
border: 1px solid var(--border-secondary);
|
||
padding: 0.4rem 0.8rem;
|
||
border-radius: 30px;
|
||
font-size: 0.8rem;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
font-family: var(--font-mono);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.graph-filter::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 0;
|
||
height: 100%;
|
||
background: var(--filter-color, 'transparent');
|
||
opacity: 0.15;
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
.graph-filter:hover::before {
|
||
width: 100%;
|
||
}
|
||
|
||
.graph-filter:hover {
|
||
color: var(--text-primary);
|
||
border-color: var(--filter-color, rgba(56, 189, 248, 0.4));
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.graph-filter.active {
|
||
background-color: var(--filter-color, var(--accent-primary));
|
||
color: var(--bg-primary);
|
||
border-color: var(--filter-color, var(--accent-primary));
|
||
font-weight: 600;
|
||
}
|
||
|
||
.graph-filter.active::before {
|
||
width: 100%;
|
||
opacity: 0.2;
|
||
}
|
||
|
||
.graph-actions {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.graph-action {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 50%;
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--border-primary);
|
||
color: var(--text-secondary);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.graph-action:hover {
|
||
color: var(--text-primary);
|
||
background: var(--bg-tertiary);
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
/* Graph Instructions are defined earlier as overlay */
|
||
/* Media Queries are defined earlier */
|
||
|
||
/* Styles for rendered post content */
|
||
.post-content {
|
||
line-height: 1.7;
|
||
font-size: 1rem;
|
||
overflow-wrap: break-word;
|
||
word-wrap: break-word;
|
||
}
|
||
|
||
.post-content * {
|
||
max-width: 100%;
|
||
}
|
||
|
||
.post-content h1, .post-content h2, .post-content h3,
|
||
.post-content h4, .post-content h5, .post-content h6 {
|
||
margin-top: 1.5rem;
|
||
margin-bottom: 1rem;
|
||
color: var(--text-primary);
|
||
font-weight: 600;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.post-content h1 { font-size: 1.875rem; }
|
||
.post-content h2 { font-size: 1.5rem; }
|
||
.post-content h3 { font-size: 1.25rem; }
|
||
.post-content h4 { font-size: 1.125rem; }
|
||
.post-content h5, .post-content h6 { font-size: 1rem; }
|
||
|
||
.post-content p {
|
||
margin-bottom: 1.25rem;
|
||
}
|
||
|
||
.post-content a {
|
||
color: var(--accent-primary);
|
||
text-decoration: none;
|
||
border-bottom: 1px dotted var(--accent-primary);
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.post-content a:hover {
|
||
color: var(--accent-secondary);
|
||
border-bottom-style: solid;
|
||
}
|
||
|
||
.post-content pre {
|
||
background: rgba(15, 23, 42, 0.6);
|
||
padding: 1rem;
|
||
border-radius: 6px;
|
||
overflow-x: auto;
|
||
margin: 1.5rem 0;
|
||
border: 1px solid var(--border-primary);
|
||
}
|
||
|
||
.post-content code {
|
||
font-family: var(--font-mono);
|
||
font-size: 0.9rem;
|
||
background: rgba(15, 23, 42, 0.4);
|
||
padding: 0.2em 0.4em;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.post-content pre code {
|
||
background: transparent;
|
||
padding: 0;
|
||
}
|
||
|
||
.post-content img {
|
||
max-width: 100%;
|
||
height: auto;
|
||
margin: 1.5rem auto;
|
||
border-radius: 6px;
|
||
display: block;
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.post-content blockquote {
|
||
border-left: 4px solid var(--accent-primary);
|
||
padding-left: 1rem;
|
||
margin-left: 0;
|
||
margin-right: 0;
|
||
font-style: italic;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.post-content ul, .post-content ol {
|
||
padding-left: 1.5rem;
|
||
margin-bottom: 1.25rem;
|
||
}
|
||
|
||
.post-content li {
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.post-content table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin: 1.5rem 0;
|
||
overflow-x: auto;
|
||
display: block;
|
||
}
|
||
|
||
.post-content table th,
|
||
.post-content table td {
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
padding: 0.5rem;
|
||
}
|
||
|
||
.post-content table th {
|
||
background: rgba(30, 41, 59, 0.5);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.post-content table tr:nth-child(even) {
|
||
background: rgba(30, 41, 59, 0.3);
|
||
}
|
||
|
||
.post-content iframe {
|
||
max-width: 100%;
|
||
margin: 1.5rem 0;
|
||
}
|
||
|
||
/* Make iframes responsive */
|
||
.post-content .video-container {
|
||
position: relative;
|
||
padding-bottom: 56.25%; /* 16:9 */
|
||
height: 0;
|
||
overflow: hidden;
|
||
margin: 1.5rem 0;
|
||
}
|
||
|
||
.post-content .video-container iframe {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
/* Loading indicator styles */
|
||
.loading-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100%;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.spinner {
|
||
width: 40px;
|
||
height: 40px;
|
||
border: 3px solid rgba(59, 130, 246, 0.3);
|
||
border-radius: 50%;
|
||
border-top-color: var(--accent-primary, #3b82f6);
|
||
animation: spin 1s linear infinite;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
</style> |