Flatten ToolsnToys structure; add edu toys, dendritic, legacy artifacts

Move 6 guide pages from Guides/ to ToolsnToys/ root; fix back-links.
Add edu-toys.html (museum-style iframe exhibit for 4 legacy edu toy pages).
Add 4 edu toy artifacts, dendritic curio, docker-cheatsheet-enhanced.
Wire foss-tools, guides, edu-toys, and dendritic hrefs in toolsntoys.html.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-02 18:08:20 +02:00
parent 0a7769d4f2
commit 73f653ff23
14 changed files with 5086 additions and 10 deletions

View 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>