diff --git a/index.html b/index.html
index 96e33c5..48ee37d 100644
--- a/index.html
+++ b/index.html
@@ -1,36 +1,92 @@
-
+
-
+
+
Phikipathia
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ Waarschuwing: De huidige boom zal overschreven worden bij een nieuwe toevoeging.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Status:
+ Bereikt
+
+
+ Stappen:
+ 0
+
+
+ Omleidingen:
+ 0
+
+
+ Paden verkend:
+ 0
+
+
+
+
+
+
diff --git a/index.js b/index.js
index 0f1e06c..9351740 100644
--- a/index.js
+++ b/index.js
@@ -1,138 +1,386 @@
-
-// TODO Optimalisatie: De huidige lijst meegeven en controleren tijdens het opstellen zodat er gestopt wordt vanaf er een titel gevonden is
-
-// Initialiseer de webpagina
-const taal = document.getElementById('taal');
-const start = document.getElementById('start');
-const einde = document.getElementById('einde');
-const toevoegKnop = document.getElementById('toevoegKnop');
-const visualisatie = document.getElementById('visualisatie');
-const logo = document.getElementById('logo');
-const waarschuwing = document.getElementById('waarschuwing');
-
-let huidigeTaal;
-let huidigeDoel;
-
-let timer;
-let rotatie;
-
-refreshTree();
-
/**
- * Zoek een pad met python backend en voeg deze toe in de lijst.
+ * Phikipathia - Philosophy Wikipedia Path Visualization
+ * Features: Clickable Wikipedia links, Collapsible Tree Nodes, Clean Branch Lines, Wikipedia Infobox, Tab Favicon Spinner
*/
-function zoek() {
+(() => {
+ 'use strict';
- // Schakel de knop 'toevoegen' uit zolang het zoekproces bezig is, zodat er geen meerdere
- // paden tegelijk toegevoegd kunnen worden.
- toggleZoeken(false);
+ // 1. Cached DOM Element References
+ const favicon = document.getElementById('favicon');
+ const taal = document.getElementById('taal');
+ const start = document.getElementById('start');
+ const einde = document.getElementById('einde');
+ const toevoegKnop = document.getElementById('toevoegKnop');
+ const wisKnop = document.getElementById('wisKnop');
+ const visualisatie = document.getElementById('visualisatie');
+ const logo = document.getElementById('logo');
+ const waarschuwing = document.getElementById('waarschuwing');
+ const errorBanner = document.getElementById('errorBanner');
+ const errorMessage = document.getElementById('errorMessage');
+ const errorDismiss = document.getElementById('errorDismiss');
- refreshTree();
+ // Wikipedia Infobox Stats Elements
+ const statistieken = document.getElementById('statistieken');
+ const statHops = document.getElementById('statHops');
+ const statStatusText = document.getElementById('statStatusText');
+ const statReroutesRow = document.getElementById('statReroutesRow');
+ const statReroutes = document.getElementById('statReroutes');
+ const statTotalPaths = document.getElementById('statTotalPaths');
- const verzoek = { // Standaardwaarden gebruiken indien het veld leeg is.
- taal: getOrDefault(taal),
- start: getOrDefault(start),
- einde: getOrDefault(einde)
- };
+ // 2. Favicon Animation Engine (Canvas Offscreen Rendering)
+ const faviconSrc = './images/wikipedia_edit.png';
+ const faviconImg = new Image();
+ faviconImg.src = faviconSrc;
- fetch(`cgi-bin/init.py?data=${JSON.stringify(verzoek)}`)
- .then(antwoord => antwoord.json())
- .then(data => voegToe(data))
- .catch(reason => alert('‼ ' + reason))
- .finally(() => toggleZoeken()); // Herstel de knop. Ook als er ondertussen iets mis ging.
+ const faviconCanvas = document.createElement('canvas');
+ faviconCanvas.width = 32;
+ faviconCanvas.height = 32;
+ const faviconCtx = faviconCanvas.getContext('2d');
-}
+ let faviconAngle = 0;
+ let faviconTimer = null;
-function voegToe(data) {
- if (data.hasOwnProperty('error')) {
- alert('↯ ' + data.error);
- } else {
- let pad = data.pad;
+ function renderFaviconFrame() {
+ if (!favicon || !faviconImg.complete || !faviconCtx) return;
+ faviconCtx.clearRect(0, 0, 32, 32);
+ faviconCtx.save();
+ faviconCtx.translate(16, 16);
+ faviconCtx.rotate((faviconAngle * Math.PI) / 180);
+ faviconCtx.drawImage(faviconImg, -16, -16, 32, 32);
+ faviconCtx.restore();
+ favicon.href = faviconCanvas.toDataURL('image/png');
+ faviconAngle = (faviconAngle + 12) % 360;
+ }
+
+ function startFaviconSpin() {
+ if (faviconTimer) return;
+ faviconAngle = 0;
+ faviconTimer = setInterval(renderFaviconFrame, 40);
+ }
+
+ function stopFaviconSpin() {
+ if (faviconTimer) {
+ clearInterval(faviconTimer);
+ faviconTimer = null;
+ }
+ if (favicon) {
+ favicon.href = faviconSrc;
+ }
+ }
+
+ // 3. Encapsulated Application State
+ let huidigeTaal = null;
+ let huidigeDoel = null;
+ let isSearching = false;
+ let totalPathsExplored = 0;
+
+ // 4. Helper: Extract input value or placeholder
+ function getOrDefault(inputElem) {
+ if (!inputElem) return '';
+ const val = inputElem.value.trim();
+ return val === '' ? (inputElem.getAttribute('placeholder') || '').trim() : val;
+ }
+
+ // Helper: Construct Wikipedia Article URL
+ function getWikiUrl(title, lang) {
+ const langCode = lang || getOrDefault(taal) || 'en';
+ const slug = encodeURIComponent(title.replace(/ /g, '_'));
+ return `https://${langCode}.wikipedia.org/wiki/${slug}`;
+ }
+
+ // 5. UI: Error & Stats Infobox Management
+ function showError(message, prefix = '↯ ') {
+ if (!errorBanner || !errorMessage) return;
+ errorMessage.textContent = `${prefix}${message}`;
+ errorBanner.hidden = false;
+ }
+
+ function hideError() {
+ if (!errorBanner) return;
+ errorBanner.hidden = true;
+ if (errorMessage) {
+ errorMessage.textContent = '';
+ }
+ }
+
+ function updateStats(data) {
+ if (!statistieken) return;
+ statistieken.hidden = false;
+
+ const hops = data.stappen !== undefined ? data.stappen : (Array.isArray(data.pad) ? Math.max(0, data.pad.length - 1) : 0);
+ if (statHops) statHops.textContent = hops;
+
+ if (statStatusText) {
+ if (data.status === 'success' || (!data.status && data.pad && !data.error)) {
+ statStatusText.textContent = `${huidigeDoel} bereikt`;
+ statStatusText.style.color = '#196f3d';
+ statStatusText.style.fontWeight = 'bold';
+ } else if (data.status === 'cyclus' || (data.error && data.error.includes('Cyclus'))) {
+ statStatusText.textContent = 'Cyclus gedetecteerd';
+ statStatusText.style.color = '#7d6608';
+ statStatusText.style.fontWeight = 'bold';
+ } else {
+ statStatusText.textContent = 'Fout / Doodlopend';
+ statStatusText.style.color = '#78281f';
+ statStatusText.style.fontWeight = 'bold';
+ }
+ }
+
+ if (statReroutesRow && statReroutes) {
+ if (data.herleid && data.herleid > 0) {
+ statReroutes.textContent = data.herleid;
+ statReroutesRow.hidden = false;
+ } else {
+ statReroutesRow.hidden = true;
+ }
+ }
+
+ if (statTotalPaths) {
+ statTotalPaths.textContent = totalPathsExplored;
+ }
+ }
+
+ // 6. UI: Dynamic Warning Visibility
+ function inputWaarschuwing() {
+ if (!waarschuwing) return;
+ const currentLangVal = getOrDefault(taal);
+ const currentTargetVal = getOrDefault(einde);
+ waarschuwing.hidden = (huidigeTaal === currentLangVal && huidigeDoel === currentTargetVal);
+ }
+
+ // 7. UI: Loading State & Animation Toggles
+ function setLoadingState(isLoading) {
+ isSearching = isLoading;
+ if (toevoegKnop) toevoegKnop.disabled = isLoading;
+ if (logo) {
+ if (isLoading) logo.classList.add('spinning');
+ else logo.classList.remove('spinning');
+ }
+ if (isLoading) {
+ startFaviconSpin();
+ hideError();
+ } else {
+ stopFaviconSpin();
+ }
+ }
+
+ // 8. DOM: Tree Node Creation
+ function insertTreeNode(title, parentElem, elementId, badgeText = null) {
+ if (!parentElem) return null;
+
+ const existingLists = parentElem.querySelectorAll(':scope > ul');
+ const isBranchSplit = existingLists.length >= 1;
+
+ if (isBranchSplit && parentElem.tagName === 'LI') {
+ parentElem.classList.add('has-split-branch');
+ // Mark primary branch (Branch 1) so a clean vertical connector line is drawn
+ if (existingLists[0] && !existingLists[0].classList.contains('branch-primary')) {
+ existingLists[0].classList.add('branch-primary');
+ }
+ }
+
+ const wrap = document.createElement('ul');
+ if (isBranchSplit) {
+ wrap.classList.add('branch-split');
+ }
+
+ const newItem = document.createElement('li');
+ newItem.setAttribute('item', title);
+
+ // Clickable Wikipedia Article Link
+ const wikiLink = document.createElement('a');
+ wikiLink.className = 'wiki-link';
+ wikiLink.href = getWikiUrl(title, huidigeTaal);
+ wikiLink.target = '_blank';
+ wikiLink.rel = 'noopener noreferrer';
+ wikiLink.textContent = title;
+ newItem.appendChild(wikiLink);
+
+ if (badgeText) {
+ const badge = document.createElement('span');
+ badge.className = 'loop-marker';
+ badge.textContent = badgeText;
+ newItem.appendChild(badge);
+ }
+
+ wrap.appendChild(newItem);
+ if (elementId !== undefined) {
+ wrap.id = elementId;
+ }
+
+ parentElem.appendChild(wrap);
+
+ // Add Collapsible toggle button to parent element
+ if (parentElem.tagName === 'LI' && !parentElem.querySelector(':scope > .toggle-btn')) {
+ const toggleBtn = document.createElement('span');
+ toggleBtn.className = 'toggle-btn';
+ toggleBtn.textContent = '˅';
+ toggleBtn.title = 'Klap in / uit';
+ toggleBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ parentElem.classList.toggle('collapsed');
+ });
+ parentElem.insertBefore(toggleBtn, parentElem.firstChild);
+ }
+
+ return newItem;
+ }
+
+ // 9. Tree Reset & Clear
+ function refreshTree() {
+ const langVal = getOrDefault(taal);
+ const targetVal = getOrDefault(einde);
+
+ if (huidigeDoel !== targetVal || huidigeTaal !== langVal) {
+ huidigeTaal = langVal;
+ huidigeDoel = targetVal;
+ totalPathsExplored = 0;
+
+ if (visualisatie) {
+ visualisatie.replaceChildren();
+ insertTreeNode(huidigeDoel, visualisatie, 'wortel');
+ }
+
+ if (statistieken) statistieken.hidden = true;
+ inputWaarschuwing();
+ }
+ }
+
+ function clearTree() {
+ if (visualisatie) {
+ visualisatie.replaceChildren();
+ insertTreeNode(getOrDefault(einde), visualisatie, 'wortel');
+ }
+ totalPathsExplored = 0;
+ if (statistieken) statistieken.hidden = true;
+ hideError();
+ inputWaarschuwing();
+ }
+
+ // 10. Path Rendering
+ function renderPath(data) {
+ if (!data) return;
+
+ totalPathsExplored += 1;
+
+ if (data.error && (!data.pad || data.pad.length === 0)) {
+ showError(data.error, '↯ ');
+ return;
+ }
+
+ const padList = Array.isArray(data.pad) ? [...data.pad] : [];
+ if (padList.length === 0) {
+ showError(data.error || 'Geen geldig pad ontvangen van de server.', '↯ ');
+ return;
+ }
+
+ let parentNode = visualisatie;
+ const existingNodes = visualisatie.getElementsByTagName('li');
+ const existingNames = Array.from(existingNodes).map(item => item.getAttribute('item'));
- // Locatie van invoegen bepalen.
- let moeder = visualisatie;
- let kinderen = visualisatie.getElementsByTagName('li');
- let kindernamen = Array.from(kinderen).map(item => item.getAttribute('item'));
let i = 0;
- while (moeder === visualisatie && i < pad.length) {
- if (kindernamen.indexOf(pad[i]) !== -1) {
- moeder = kinderen[kindernamen.indexOf(pad[i])];
+ while (parentNode === visualisatie && i < padList.length) {
+ const foundIdx = existingNames.indexOf(padList[i]);
+ if (foundIdx !== -1) {
+ parentNode = existingNodes[foundIdx];
}
i += 1;
}
- // Alleen nog die elementen toevoegen die nog niet op het pad staan.
- pad = pad.slice(0, i - 1);
+ if (parentNode === visualisatie) {
+ const rootLi = visualisatie.querySelector('#wortel li');
+ if (rootLi) {
+ parentNode = rootLi;
+ }
+ }
- // Nieuw element creëren (zonder toe te voegen aan het DOM).
- for (let item of pad.reverse()) {
- moeder = maakIn(item, moeder);
+ const remainingItems = padList.slice(0, i - 1);
+ const reversedItems = remainingItems.reverse();
+ for (let idx = 0; idx < reversedItems.length; idx++) {
+ const item = reversedItems[idx];
+ let badge = null;
+ if (idx === 0 && data.status === 'cyclus') {
+ badge = 'cyclus';
+ }
+ parentNode = insertTreeNode(item, parentNode, undefined, badge);
+ }
+
+ updateStats(data);
+
+ if (data.error) {
+ showError(data.error, 'ℹ ');
}
}
-}
-/**
- * Maakt een element aan binnen een ander element, en geeft het nieuwste (diepste) element terug.
- */
-function maakIn(titel, moeder, id) {
- let wrap = document.createElement('ul');
- let nieuw = document.createElement('li');
- nieuw.innerHTML = titel;
- nieuw.setAttribute('item', titel);
- wrap.appendChild(nieuw);
+ // 11. API Search Execution
+ function zoek() {
+ if (isSearching) return;
- if (id !== undefined) {
- wrap.id = id;
+ setLoadingState(true);
+ refreshTree();
+
+ const verzoek = {
+ taal: getOrDefault(taal),
+ start: getOrDefault(start),
+ einde: getOrDefault(einde)
+ };
+
+ const encodedData = encodeURIComponent(JSON.stringify(verzoek));
+ const endpointUrl = `cgi-bin/init.py?data=${encodedData}`;
+
+ fetch(endpointUrl)
+ .then(response => {
+ if (!response.ok) {
+ return response.json()
+ .then(errJson => {
+ throw new Error(errJson.error || `Server HTTP ${response.status}`);
+ })
+ .catch(parseErr => {
+ if (parseErr.message && !parseErr.message.includes('JSON')) throw parseErr;
+ throw new Error(`Server HTTP ${response.status}`);
+ });
+ }
+ return response.json();
+ })
+ .then(data => {
+ renderPath(data);
+ })
+ .catch(err => {
+ showError(err.message || 'Er is een netwerkfout opgetreden.', '‼ ');
+ })
+ .finally(() => {
+ setLoadingState(false);
+ });
}
- moeder.appendChild(wrap);
- return nieuw;
-}
+ // 12. Event Binding & Initialization
+ function init() {
+ if (toevoegKnop) toevoegKnop.addEventListener('click', zoek);
+ if (wisKnop) wisKnop.addEventListener('click', clearTree);
+ if (taal) {
+ taal.addEventListener('input', inputWaarschuwing);
+ taal.addEventListener('change', inputWaarschuwing);
+ }
+ if (einde) einde.addEventListener('input', inputWaarschuwing);
+ if (errorDismiss) errorDismiss.addEventListener('click', hideError);
-/**
- * Probeer of de huidige boom verwijderd moet worden en voeg de bestemming reeds toe aan de nieuwe boom.
- */
-function refreshTree() {
- if (huidigeDoel !== getOrDefault(einde) || huidigeTaal !== getOrDefault(taal)) {
- huidigeTaal = getOrDefault(taal);
- huidigeDoel = getOrDefault(einde);
+ [taal, start, einde].forEach(input => {
+ if (input) {
+ input.addEventListener('keydown', (event) => {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ zoek();
+ }
+ });
+ }
+ });
- visualisatie.innerHTML = '';
- maakIn(huidigeDoel, visualisatie, 'wortel');
-
- inputWaarschuwing();
+ refreshTree();
}
-}
-/**
- * Toon of verberg een speciale waarschuwing die zegt of de huidige boom verwijderd zal worden
- * bij een nieuwe toevoeging.
- */
-function inputWaarschuwing() {
- waarschuwing.hidden = (huidigeTaal === getOrDefault(taal) && huidigeDoel === getOrDefault(einde));
-}
-
-function getOrDefault(element) {
- return element.value === '' ? element.getAttribute('placeholder') : element.value;
-}
-
-/**
- * Toggle tussen de zoek-status en de wacht-op-input-status. Dit heeft invloed op zowel de indienkop als op het
- * roterende logo. Standaardaanroep zal alles stoppen, om überveel animaties te vermijden bij fout gebruik.
- */
-function toggleZoeken(stop = true) {
- toevoegKnop.disabled = !stop;
-
- rotatie = 0;
- if (stop) {
- clearInterval(timer);
- logo.style.transform = 'rotate(0)';
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
} else {
- timer = setInterval(draai, 24);
+ init();
}
-}
-
-function draai() {
- rotatie += 5;
- logo.style.transform = `rotate(${rotatie % 360}deg`;
-}
+})();
diff --git a/style.css b/style.css
index 02be329..bd84e7a 100644
--- a/style.css
+++ b/style.css
@@ -1,17 +1,16 @@
-/*
-Waarschijnlijk eerder de enigste file die niet deftig gedocumenteerd is.
-Ach, de kans dat jullie dit bekijken is hoe dan ook klein.
- */
-
/* Algemeen */
body {
- font-family: 'Linux Libertine','Georgia','Times',serif;
+ font-family: 'Linux Libertine', 'Georgia', 'Times', serif;
padding: 0 1.5em 1.5em 1.5em;
line-height: 1.6;
+ margin: 0;
+ color: #202122;
+ background-color: #ffffff;
}
h1 {
+ font-family: 'Linux Libertine', 'Georgia', 'Times', serif;
margin-bottom: 0.25em;
padding: 0;
line-height: 1.3;
@@ -20,97 +19,365 @@ h1 {
border-bottom: 1px solid #a2a9b1;
}
-li ul {
- margin: 0.2vh 1ch 0 1ch;
- padding: 0.2vh 1vw;
-}
-
/* Header */
+.header {
+ display: flex;
+ align-items: center;
+ gap: 0.8em;
+ padding: 0.5em 0;
+}
+
.header img {
- height: 1.8em;
- margin-top: 0.3em;
- float: left;
+ height: 2.2em;
+ width: auto;
+ flex-shrink: 0;
+}
+
+.header-content {
+ flex: 1;
+}
+
+.header h1 {
+ font-family: 'Linux Libertine', 'Georgia', 'Times', serif;
+ margin: 0;
+ padding: 0;
+ line-height: 1.2;
+ font-size: 1.8em;
+ font-weight: normal;
+ border-bottom: none;
}
.header p {
+ font-family: 'Linux Libertine', 'Georgia', 'Times', serif;
+ margin: 0.2em 0 0 0;
font-size: 92%;
- display: block;
color: #202122;
- font-family: sans-serif;
}
-/* Box */
+/* Loading Animation */
+
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.spinning {
+ animation: spin 1.2s linear infinite;
+ will-change: transform;
+}
+
+/* Controls & Box Layout */
.box {
font-family: sans-serif;
font-size: 95%;
-
- padding: 7px;
+ padding: 10px 14px;
border: 1px solid #a2a9b1;
background-color: #f8f9fa;
color: #202122;
-
- position: sticky;
- top: 7px;
}
-.box p {
- padding: 0;
- margin: 0;
+main > .box:first-child {
+ position: sticky;
+ top: 7px;
+ z-index: 10;
+}
+
+.form-row {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px 12px;
+}
+
+.form-group {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.form-group input[type="text"],
+.form-group select {
+ padding: 3px 6px;
+ border: 1px solid #a2a9b1;
+ border-radius: 2px;
+ font-size: 0.95em;
+ font-family: sans-serif;
+ background-color: #ffffff;
+}
+
+#taal {
+ min-width: 9em;
+}
+
+#start, #einde {
+ min-width: 140px;
+ flex: 1 1 auto;
+}
+
+/* Full-Width Statistics Box at Bottom */
+
+.stats-box {
+ margin-top: 1.2em;
+}
+
+.stats-box[hidden] {
+ display: none !important;
+}
+
+.stats-row {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 12px 24px;
+ font-size: 92%;
+}
+
+.stat-item {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.stat-label {
+ color: #54595d;
+ font-weight: bold;
+}
+
+.stat-val {
+ color: #202122;
+}
+
+.stats-row button#wisKnop {
+ margin-left: auto;
+}
+
+/* Warning & Error Banners */
+
+#waarschuwing {
+ display: block;
+ margin-top: 8px;
+ font-size: 90%;
+ color: #54595d;
+}
+
+#waarschuwing[hidden] {
+ display: none !important;
}
#waarschuwing b {
color: #ba0000;
}
-/* Visualisatie */
+.error-banner {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 8px;
+ padding: 8px 12px;
+ background-color: #fdf2f2;
+ border: 1px solid #d33;
+ border-left: 4px solid #ba0000;
+ color: #721c24;
+ border-radius: 2px;
+ font-size: 90%;
+ animation: fadeIn 0.2s ease-in-out;
+}
+
+.error-banner[hidden] {
+ display: none !important;
+}
+
+.error-message {
+ flex: 1;
+ word-break: break-word;
+}
+
+.error-dismiss {
+ background: none;
+ border: none;
+ color: #ba0000;
+ font-size: 1.3em;
+ line-height: 1;
+ cursor: pointer;
+ padding: 0 4px;
+ margin-left: 8px;
+}
+
+.error-dismiss:hover {
+ color: #000;
+ text-decoration: none;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(-3px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+/* Wikipedia Table of Contents (TOC) Style Visualisatie */
#visualisatie {
- margin: 0.5em 0;
- font-size: 1em;
+ margin: 1em 0;
+ font-family: sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
+ font-size: 0.92em;
+ line-height: 1.7;
+ overflow-x: auto;
+ max-width: 100%;
+ padding-bottom: 0.5em;
}
#visualisatie ul {
- list-style-type: symbols("⬑"); /* alternatieven "↪" "⬉" */
+ list-style-type: none;
+ padding-left: 1.2em;
+ margin: 0.15em 0;
}
-
ul#wortel {
- padding: 0;
+ padding-left: 0;
list-style-type: none;
+ font-weight: bold;
+}
+
+ul#wortel > li > a.wiki-link {
+ font-weight: bold;
+ color: #202122;
+ font-size: 1.05em;
+}
+
+#visualisatie li {
+ padding: 0.1em 0;
+ position: relative;
+ white-space: nowrap;
+}
+
+/* Clickable Wikipedia Article Links - Wikipedia Blue */
+#visualisatie a.wiki-link {
+ color: #3366bb;
+ text-decoration: none;
+ white-space: nowrap;
+ transition: color 0.1s, text-decoration 0.1s;
+}
+
+#visualisatie a.wiki-link:hover {
+ color: #0645ad;
+ text-decoration: underline;
+}
+
+#visualisatie a.wiki-link:visited {
+ color: #0b0080;
+}
+
+/* Pixel-Perfect Tree Connector Lines:
+ - Primary branch (Branch 1) draws a clean vertical line starting at parent and ending at split point.
+ - Secondary branch (Branch 2) draws a horizontal tick starting at x=0 that seamlessly meets the vertical line. */
+
+#visualisatie ul.branch-primary {
+ border-left: 2px solid #3366bb;
+ padding-left: 1.2em;
+ margin-left: 0;
+}
+
+#visualisatie ul.branch-split {
+ margin-top: 6px;
+ padding-left: 1.2em;
+ position: relative;
+}
+
+#visualisatie ul.branch-split::before {
+ content: "";
+ position: absolute;
+ top: 0.85em;
+ left: 0;
+ width: 0.85em;
+ height: 0;
+ border-top: 2px solid #3366bb;
+}
+
+/* Collapsible Node Toggle Chevron (Wikipedia Style) */
+.toggle-btn {
+ display: inline-block;
+ cursor: pointer;
+ user-select: none;
+ font-size: 70%;
+ margin-right: 5px;
+ color: #72777d;
+ transition: transform 0.15s ease;
+ width: 0.9em;
+ text-align: center;
+ vertical-align: middle;
+}
+
+.toggle-btn:hover {
+ color: #202122;
+}
+
+li.collapsed > ul {
+ display: none !important;
+}
+
+li.collapsed > .toggle-btn {
+ transform: rotate(-90deg);
+}
+
+/* Loop node marker in tree */
+
+.loop-marker {
+ display: inline-block;
+ margin-left: 6px;
+ padding: 1px 6px;
+ font-size: 80%;
+ font-family: sans-serif;
+ border-radius: 2px;
+ background-color: #fcf3cf;
+ color: #7d6608;
+ border: 1px solid #f9e79f;
+ font-weight: normal;
}
/* Footer */
footer {
text-align: center;
+ margin-top: 1.5em;
}
-/* Wikipedia-achtige knop */
+/* Wikipedia Buttons */
button {
cursor: pointer;
border: none;
background: none;
color: #3366bb;
+ font-size: 0.95em;
}
button:disabled {
- color: #ba0000;
+ color: #72777d;
+ cursor: not-allowed;
}
-button:hover {
+button:not(:disabled):hover {
text-decoration: underline;
}
-button:before {
+#toevoegKnop:before {
color: #54595d;
margin-right: 0.25em;
content: "[";
}
-button:after {
+#toevoegKnop:after {
color: #54595d;
margin-left: 0.25em;
content: "]";