Initial commit — Singular Particular Space v1
Homepage (site/index.html): integration-v14 promoted, Writings section integrated with 33 pieces clustered by type (stories/essays/miscellany), Writings welcome lightbox, content frame at 98% opacity. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
828
DumperCan/dendritic_links_network_v23.html
Executable file
828
DumperCan/dendritic_links_network_v23.html
Executable file
@@ -0,0 +1,828 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dendritic Links Network</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background: linear-gradient(135deg, #0B0B0B 0%, #69190D 100%);
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #F2F2BC;
|
||||
overflow-x: hidden;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
padding: 2rem 1rem;
|
||||
background: rgba(11, 11, 11, 0.8);
|
||||
border-bottom: 3px solid;
|
||||
border-image: linear-gradient(90deg, #69190D, #A52C16, #EB7513, #F2C62A, #F2F2BC) 1;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: 'Arial Black', 'Impact', sans-serif;
|
||||
font-size: clamp(1.5rem, 5vw, 3rem);
|
||||
color: #EB7513;
|
||||
text-shadow: 0 0 20px rgba(235, 117, 19, 0.5);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: clamp(0.8rem, 2vw, 1rem);
|
||||
color: #F2C62A;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
flex-wrap: wrap;
|
||||
background: rgba(11, 11, 11, 0.6);
|
||||
border-bottom: 2px solid #442204;
|
||||
}
|
||||
|
||||
.btn-control {
|
||||
padding: 0.6rem 1.2rem;
|
||||
background: linear-gradient(135deg, #442204, #69190D);
|
||||
color: #F2C62A;
|
||||
border: 2px solid #F2C62A;
|
||||
border-radius: 5px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-control:hover {
|
||||
background: linear-gradient(135deg, #69190D, #A52C16);
|
||||
border-color: #EB7513;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 20px rgba(235, 117, 19, 0.4);
|
||||
}
|
||||
|
||||
#file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#network-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: calc(100vh - 250px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
svg:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.branch {
|
||||
stroke: #442204;
|
||||
stroke-width: 2;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.category-node {
|
||||
cursor: move;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.category-node:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.category-node circle {
|
||||
fill: #442204;
|
||||
stroke: #EB7513;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.category-node text {
|
||||
fill: #EB7513;
|
||||
font-family: 'Arial Black', 'Impact', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-anchor: middle;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.leaf-node {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.leaf-node circle {
|
||||
fill: #073720;
|
||||
stroke: #5E9A0E;
|
||||
stroke-width: 2;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.leaf-node:hover circle {
|
||||
fill: #69190D;
|
||||
stroke: #F2C62A;
|
||||
transform: scale(1.2);
|
||||
filter: drop-shadow(0 0 10px #F2C62A);
|
||||
}
|
||||
|
||||
.leaf-node text {
|
||||
fill: #F2F2BC;
|
||||
font-size: 10px;
|
||||
text-anchor: middle;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.leaf-node:hover text {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Lightbox styles */
|
||||
.lightbox {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(11, 11, 11, 0.95);
|
||||
z-index: 1000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.lightbox.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.lightbox-content {
|
||||
background: linear-gradient(135deg, #0B0B0B, #442204);
|
||||
border: 3px solid;
|
||||
border-image: linear-gradient(90deg, #69190D, #A52C16, #EB7513, #F2C62A) 1;
|
||||
border-radius: 10px;
|
||||
padding: 2rem;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 0 40px rgba(235, 117, 19, 0.5);
|
||||
}
|
||||
|
||||
.lightbox-content h2 {
|
||||
font-family: 'Arial Black', 'Impact', sans-serif;
|
||||
color: #EB7513;
|
||||
margin-bottom: 1rem;
|
||||
font-size: clamp(1.2rem, 3vw, 1.8rem);
|
||||
}
|
||||
|
||||
.lightbox-content p {
|
||||
color: #F2F2BC;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.lightbox-content textarea {
|
||||
width: 100%;
|
||||
min-height: 300px;
|
||||
background: #0B0B0B;
|
||||
color: #F2F2BC;
|
||||
border: 2px solid #442204;
|
||||
border-radius: 5px;
|
||||
padding: 1rem;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9rem;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.lightbox-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
padding: 0.8rem 1.5rem;
|
||||
border: 2px solid #F2C62A;
|
||||
background: linear-gradient(135deg, #442204, #69190D);
|
||||
color: #F2C62A;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: linear-gradient(135deg, #69190D, #A52C16);
|
||||
color: #F2F2BC;
|
||||
border-color: #EB7513;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 20px rgba(235, 117, 19, 0.4);
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
background: linear-gradient(135deg, #5E9A0E, #073720);
|
||||
border-color: #5E9A0E;
|
||||
}
|
||||
|
||||
.btn-close:hover {
|
||||
background: linear-gradient(135deg, #073720, #0E4D0E);
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
font-size: 1.2rem;
|
||||
color: #EB7513;
|
||||
}
|
||||
|
||||
.zoom-indicator {
|
||||
position: fixed;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
background: rgba(11, 11, 11, 0.9);
|
||||
border: 2px solid #F2C62A;
|
||||
border-radius: 5px;
|
||||
padding: 0.5rem 1rem;
|
||||
color: #F2C62A;
|
||||
font-size: 0.9rem;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.zoom-indicator.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.reset-zoom {
|
||||
position: fixed;
|
||||
bottom: 1rem;
|
||||
left: 1rem;
|
||||
background: rgba(11, 11, 11, 0.9);
|
||||
border: 2px solid #5E9A0E;
|
||||
border-radius: 5px;
|
||||
padding: 0.5rem 1rem;
|
||||
color: #5E9A0E;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.reset-zoom:hover {
|
||||
background: rgba(94, 154, 14, 0.2);
|
||||
border-color: #F2C62A;
|
||||
color: #F2C62A;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Mobile optimizations */
|
||||
@media (max-width: 768px) {
|
||||
header {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.controls {
|
||||
padding: 0.5rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-control {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.subtitle br {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.lightbox-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
min-width: 100px;
|
||||
padding: 0.6rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.category-node text {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.reset-zoom {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.4rem 0.8rem;
|
||||
}
|
||||
|
||||
.zoom-indicator {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.4rem 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
svg {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.category-node {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.leaf-node {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>🌳 Dendritic Links Network</h1>
|
||||
<p class="subtitle">Your Browser Tabs, Visualized</p>
|
||||
<p class="subtitle" style="margin-top: 0.5rem; font-size: 0.85em; opacity: 0.8;">
|
||||
🖱️ Drag to pan • 🔍 Scroll/Pinch to zoom • 🖱️ Right-drag to zoom • 👆 Click leaves to explore<br>
|
||||
<span style="font-size: 0.9em; opacity: 0.7;">⌨️ Keyboard: R = Reset • +/- = Zoom</span>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="controls">
|
||||
<button class="btn-control" onclick="document.getElementById('file-input').click()">
|
||||
📁 Load Markdown File
|
||||
</button>
|
||||
<button class="btn-control" onclick="showPasteDialog()">
|
||||
📋 Paste Markdown
|
||||
</button>
|
||||
<button class="btn-control" onclick="loadDefaultData()">
|
||||
🎨 Load Demo Data
|
||||
</button>
|
||||
<input type="file" id="file-input" accept=".md,.markdown,.txt" onchange="handleFileUpload(event)">
|
||||
</div>
|
||||
|
||||
<div id="network-container">
|
||||
<div class="loading">🌱 Click a button above to load your links!</div>
|
||||
</div>
|
||||
|
||||
<button class="reset-zoom" id="reset-zoom" title="Reset zoom and position">
|
||||
🎯 Reset View
|
||||
</button>
|
||||
|
||||
<div class="zoom-indicator" id="zoom-indicator">
|
||||
Zoom: <span id="zoom-level">100%</span>
|
||||
</div>
|
||||
|
||||
<!-- Link detail lightbox -->
|
||||
<div class="lightbox" id="lightbox">
|
||||
<div class="lightbox-content">
|
||||
<h2 id="lightbox-title"></h2>
|
||||
<p id="lightbox-description"></p>
|
||||
<div class="lightbox-buttons">
|
||||
<a id="visit-link" class="btn" href="#" target="_blank">🔗 Visit Link</a>
|
||||
<button class="btn btn-close" onclick="closeLightbox()">✕ Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Paste markdown lightbox -->
|
||||
<div class="lightbox" id="paste-lightbox">
|
||||
<div class="lightbox-content">
|
||||
<h2>Paste Your Markdown</h2>
|
||||
<p>Paste your organized links markdown below:</p>
|
||||
<textarea id="markdown-input" placeholder="# Your Links
|
||||
|
||||
## Category Name
|
||||
|
||||
- [Link Name](https://example.com/)
|
||||
- Description of the link goes here.
|
||||
|
||||
## Another Category
|
||||
|
||||
..."></textarea>
|
||||
<div class="lightbox-buttons">
|
||||
<button class="btn" onclick="processPastedMarkdown()">✨ Generate Network</button>
|
||||
<button class="btn btn-close" onclick="closePasteDialog()">✕ Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/markdown-it/13.0.1/markdown-it.min.js"></script>
|
||||
<script>
|
||||
console.log("🌳 Navigation: Drag to pan • Scroll/Pinch to zoom • Right-click+Drag to zoom • Click nodes to interact");
|
||||
|
||||
let currentSimulation = null;
|
||||
let currentSvg = null;
|
||||
|
||||
// Markdown parser
|
||||
const md = window.markdownit();
|
||||
|
||||
// Parse markdown into linkData structure
|
||||
function parseMarkdownToLinkData(markdown) {
|
||||
const tokens = md.parse(markdown, {});
|
||||
const linkData = {};
|
||||
let currentCategory = null;
|
||||
let currentLink = null;
|
||||
let expectingDescription = false;
|
||||
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
const token = tokens[i];
|
||||
|
||||
// Detect h2 headers (## Category Name)
|
||||
if (token.type === 'heading_open' && token.tag === 'h2') {
|
||||
const nextToken = tokens[i + 1];
|
||||
if (nextToken && nextToken.type === 'inline') {
|
||||
currentCategory = nextToken.content.trim();
|
||||
linkData[currentCategory] = [];
|
||||
currentLink = null;
|
||||
expectingDescription = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Detect links inside list items
|
||||
if (token.type === 'inline' && token.children && currentCategory) {
|
||||
const linkToken = token.children.find(t => t.type === 'link_open');
|
||||
if (linkToken) {
|
||||
const textToken = token.children.find(t => t.type === 'text');
|
||||
if (textToken) {
|
||||
const url = linkToken.attrGet('href');
|
||||
currentLink = {
|
||||
name: textToken.content,
|
||||
url: url,
|
||||
desc: ''
|
||||
};
|
||||
expectingDescription = true;
|
||||
}
|
||||
} else if (expectingDescription && currentLink) {
|
||||
// This is the description line
|
||||
currentLink.desc = token.content.trim();
|
||||
linkData[currentCategory].push(currentLink);
|
||||
expectingDescription = false;
|
||||
currentLink = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Also check paragraph content for descriptions
|
||||
if (token.type === 'paragraph_open' && expectingDescription && currentLink) {
|
||||
const nextToken = tokens[i + 1];
|
||||
if (nextToken && nextToken.type === 'inline') {
|
||||
// Skip if this contains a link (it's a new link item)
|
||||
if (!nextToken.children.some(t => t.type === 'link_open')) {
|
||||
currentLink.desc = nextToken.content.trim();
|
||||
linkData[currentCategory].push(currentLink);
|
||||
expectingDescription = false;
|
||||
currentLink = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up any categories with no links
|
||||
Object.keys(linkData).forEach(key => {
|
||||
if (linkData[key].length === 0) {
|
||||
delete linkData[key];
|
||||
}
|
||||
});
|
||||
|
||||
return linkData;
|
||||
}
|
||||
|
||||
// Create the force-directed graph
|
||||
function createNetwork(linkData) {
|
||||
const container = document.getElementById('network-container');
|
||||
const width = container.clientWidth;
|
||||
const height = container.clientHeight;
|
||||
|
||||
// Clear existing content
|
||||
container.innerHTML = '';
|
||||
|
||||
// Stop existing simulation if any
|
||||
if (currentSimulation) {
|
||||
currentSimulation.stop();
|
||||
}
|
||||
|
||||
const svg = d3.select('#network-container')
|
||||
.append('svg')
|
||||
.attr('width', width)
|
||||
.attr('height', height);
|
||||
|
||||
currentSvg = svg;
|
||||
|
||||
const g = svg.append('g');
|
||||
|
||||
// Setup zoom behavior
|
||||
let zoomTimeout;
|
||||
const zoomIndicator = document.getElementById('zoom-indicator');
|
||||
const zoomLevel = document.getElementById('zoom-level');
|
||||
|
||||
const zoom = d3.zoom()
|
||||
.scaleExtent([0.1, 4])
|
||||
.on('zoom', (event) => {
|
||||
g.attr('transform', event.transform);
|
||||
|
||||
zoomIndicator.classList.add('visible');
|
||||
zoomLevel.textContent = Math.round(event.transform.k * 100) + '%';
|
||||
|
||||
clearTimeout(zoomTimeout);
|
||||
zoomTimeout = setTimeout(() => {
|
||||
zoomIndicator.classList.remove('visible');
|
||||
}, 1500);
|
||||
});
|
||||
|
||||
svg.call(zoom);
|
||||
|
||||
// Right-click zoom
|
||||
let rightDragStart = null;
|
||||
svg.on('contextmenu', (event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
svg.on('mousedown', (event) => {
|
||||
if (event.button === 2) {
|
||||
rightDragStart = { y: event.clientY, scale: d3.zoomTransform(svg.node()).k };
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
svg.on('mousemove', (event) => {
|
||||
if (rightDragStart && event.buttons === 2) {
|
||||
const delta = (rightDragStart.y - event.clientY) * 0.01;
|
||||
const newScale = Math.max(0.1, Math.min(4, rightDragStart.scale * (1 + delta)));
|
||||
svg.call(zoom.scaleTo, newScale);
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
svg.on('mouseup', (event) => {
|
||||
if (event.button === 2) {
|
||||
rightDragStart = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Create nodes and links
|
||||
const nodes = [];
|
||||
const links = [];
|
||||
|
||||
nodes.push({ id: 'center', type: 'center', x: width / 2, y: height / 2 });
|
||||
|
||||
let nodeId = 0;
|
||||
Object.keys(linkData).forEach((category, catIndex) => {
|
||||
const catId = `cat-${catIndex}`;
|
||||
nodes.push({
|
||||
id: catId,
|
||||
type: 'category',
|
||||
label: category,
|
||||
fixed: false
|
||||
});
|
||||
links.push({ source: 'center', target: catId });
|
||||
|
||||
linkData[category].forEach((link, linkIndex) => {
|
||||
const leafId = `leaf-${nodeId++}`;
|
||||
nodes.push({
|
||||
id: leafId,
|
||||
type: 'leaf',
|
||||
label: link.name,
|
||||
url: link.url,
|
||||
description: link.desc
|
||||
});
|
||||
links.push({ source: catId, target: leafId });
|
||||
});
|
||||
});
|
||||
|
||||
// Create force simulation
|
||||
const simulation = d3.forceSimulation(nodes)
|
||||
.force('link', d3.forceLink(links).id(d => d.id).distance(d => d.source.type === 'category' ? 150 : 80))
|
||||
.force('charge', d3.forceManyBody().strength(-300))
|
||||
.force('center', d3.forceCenter(width / 2, height / 2))
|
||||
.force('collision', d3.forceCollide().radius(30));
|
||||
|
||||
currentSimulation = simulation;
|
||||
|
||||
// Create links
|
||||
const link = g.append('g')
|
||||
.selectAll('line')
|
||||
.data(links)
|
||||
.enter()
|
||||
.append('line')
|
||||
.attr('class', 'branch')
|
||||
.attr('stroke-opacity', 0.6);
|
||||
|
||||
// Create nodes
|
||||
const node = g.append('g')
|
||||
.selectAll('g')
|
||||
.data(nodes.filter(n => n.type !== 'center'))
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr('class', d => d.type === 'category' ? 'category-node' : 'leaf-node')
|
||||
.call(d3.drag()
|
||||
.on('start', dragstarted)
|
||||
.on('drag', dragged)
|
||||
.on('end', dragended));
|
||||
|
||||
node.append('circle')
|
||||
.attr('r', d => d.type === 'category' ? 20 : 12);
|
||||
|
||||
node.append('text')
|
||||
.text(d => d.label)
|
||||
.attr('dy', d => d.type === 'category' ? -25 : -18);
|
||||
|
||||
// Click handler for leaf nodes
|
||||
node.filter(d => d.type === 'leaf')
|
||||
.on('click', function(event, d) {
|
||||
event.stopPropagation();
|
||||
openLightbox(d.label, d.description, d.url);
|
||||
});
|
||||
|
||||
// Update positions
|
||||
simulation.on('tick', () => {
|
||||
link
|
||||
.attr('x1', d => d.source.x)
|
||||
.attr('y1', d => d.source.y)
|
||||
.attr('x2', d => d.target.x)
|
||||
.attr('y2', d => d.target.y);
|
||||
|
||||
node
|
||||
.attr('transform', d => `translate(${d.x},${d.y})`);
|
||||
});
|
||||
|
||||
// Drag functions
|
||||
function dragstarted(event, d) {
|
||||
if (!event.active) simulation.alphaTarget(0.3).restart();
|
||||
d.fx = d.x;
|
||||
d.fy = d.y;
|
||||
}
|
||||
|
||||
function dragged(event, d) {
|
||||
d.fx = event.x;
|
||||
d.fy = event.y;
|
||||
}
|
||||
|
||||
function dragended(event, d) {
|
||||
if (!event.active) simulation.alphaTarget(0);
|
||||
d.fx = null;
|
||||
d.fy = null;
|
||||
}
|
||||
|
||||
// Reset zoom button
|
||||
document.getElementById('reset-zoom').onclick = () => {
|
||||
svg.transition()
|
||||
.duration(750)
|
||||
.call(zoom.transform, d3.zoomIdentity);
|
||||
};
|
||||
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener('keydown', (e) => {
|
||||
switch(e.key.toLowerCase()) {
|
||||
case 'r':
|
||||
svg.transition()
|
||||
.duration(750)
|
||||
.call(zoom.transform, d3.zoomIdentity);
|
||||
break;
|
||||
case '+':
|
||||
case '=':
|
||||
svg.transition()
|
||||
.duration(300)
|
||||
.call(zoom.scaleBy, 1.3);
|
||||
break;
|
||||
case '-':
|
||||
case '_':
|
||||
svg.transition()
|
||||
.duration(300)
|
||||
.call(zoom.scaleBy, 0.7);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle file upload
|
||||
function handleFileUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
const markdown = e.target.result;
|
||||
try {
|
||||
const linkData = parseMarkdownToLinkData(markdown);
|
||||
if (Object.keys(linkData).length === 0) {
|
||||
alert('No links found in the markdown file. Please check the format.');
|
||||
return;
|
||||
}
|
||||
createNetwork(linkData);
|
||||
} catch (error) {
|
||||
console.error('Error parsing markdown:', error);
|
||||
alert('Error parsing markdown file. Please check the format.');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
// Show paste dialog
|
||||
function showPasteDialog() {
|
||||
document.getElementById('paste-lightbox').classList.add('active');
|
||||
document.getElementById('markdown-input').focus();
|
||||
}
|
||||
|
||||
// Close paste dialog
|
||||
function closePasteDialog() {
|
||||
document.getElementById('paste-lightbox').classList.remove('active');
|
||||
}
|
||||
|
||||
// Process pasted markdown
|
||||
function processPastedMarkdown() {
|
||||
const markdown = document.getElementById('markdown-input').value;
|
||||
if (!markdown.trim()) {
|
||||
alert('Please paste some markdown content.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const linkData = parseMarkdownToLinkData(markdown);
|
||||
if (Object.keys(linkData).length === 0) {
|
||||
alert('No links found in the markdown. Please check the format.');
|
||||
return;
|
||||
}
|
||||
closePasteDialog();
|
||||
createNetwork(linkData);
|
||||
} catch (error) {
|
||||
console.error('Error parsing markdown:', error);
|
||||
alert('Error parsing markdown. Please check the format.');
|
||||
}
|
||||
}
|
||||
|
||||
// Load default demo data
|
||||
function loadDefaultData() {
|
||||
const demoData = {
|
||||
"AI & Machine Learning": [
|
||||
{ name: "Z.ai Chat", url: "https://chat.z.ai/", desc: "Free AI chat powered by GLM models." },
|
||||
{ name: "DeepSeek", url: "https://chat.deepseek.com/", desc: "Another AI assistant." },
|
||||
{ name: "BlueDot Impact", url: "https://bluedot.org/", desc: "Industry-leading free AI courses." }
|
||||
],
|
||||
"Development Tools": [
|
||||
{ name: "Context7 MCP", url: "https://github.com/upstash/context7", desc: "Up-to-date code docs for LLMs." },
|
||||
{ name: "TypeScript Docs", url: "https://www.typescriptlang.org/docs/", desc: "JavaScript's responsible sibling." },
|
||||
{ name: "DevDocs", url: "https://devdocs.io/", desc: "All your API docs in one place." }
|
||||
],
|
||||
"Creative & Design": [
|
||||
{ name: "GIMP", url: "https://www.gimp.org/", desc: "Free Photoshop alternative." },
|
||||
{ name: "Blender", url: "https://www.blender.org/", desc: "Free 3D creation suite." },
|
||||
{ name: "Inkscape", url: "https://inkscape.org/", desc: "Vector graphics editor." }
|
||||
],
|
||||
"Knowledge Management": [
|
||||
{ name: "Logseq", url: "https://logseq.com/", desc: "Privacy-first knowledge base." },
|
||||
{ name: "Trilium Notes", url: "https://triliumnotes.org/", desc: "Hierarchical note-taking." },
|
||||
{ name: "BookStack", url: "https://www.bookstackapp.com/", desc: "Organize docs like a library." }
|
||||
]
|
||||
};
|
||||
createNetwork(demoData);
|
||||
}
|
||||
|
||||
// Lightbox functions
|
||||
function openLightbox(title, description, url) {
|
||||
document.getElementById('lightbox-title').textContent = title;
|
||||
document.getElementById('lightbox-description').textContent = description;
|
||||
document.getElementById('visit-link').href = url;
|
||||
document.getElementById('lightbox').classList.add('active');
|
||||
}
|
||||
|
||||
function closeLightbox() {
|
||||
document.getElementById('lightbox').classList.remove('active');
|
||||
}
|
||||
|
||||
// Close lightbox on background click
|
||||
document.getElementById('lightbox').addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeLightbox();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('paste-lightbox').addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closePasteDialog();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user