knowledgebase style
This commit is contained in:
@@ -52,205 +52,430 @@ const currentUrl = Astro.url.href;
|
||||
---
|
||||
|
||||
<BaseLayout title={entry.data.title} description={entry.data.description}>
|
||||
<article style="max-width: 900px; margin: 0 auto;">
|
||||
<header style="margin-bottom: 2rem; padding: 2rem; background: linear-gradient(135deg, var(--color-bg-secondary) 0%, var(--color-bg-tertiary) 100%); border-radius: 1rem; border: 1px solid var(--color-border);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem;">
|
||||
<div style="flex: 1;">
|
||||
<h1 style="margin: 0 0 0.5rem 0; color: var(--color-primary);">
|
||||
{displayTool?.icon && <span style="margin-right: 0.75rem; font-size: 1.5rem;">{displayTool.icon}</span>}
|
||||
<div class="article-layout">
|
||||
<!-- Article Header -->
|
||||
<header class="article-header">
|
||||
<div class="article-header-content">
|
||||
<div class="article-meta">
|
||||
<nav class="breadcrumb">
|
||||
<a href="/knowledgebase" class="breadcrumb-link">Knowledgebase</a>
|
||||
<span class="breadcrumb-separator">→</span>
|
||||
<span class="breadcrumb-current">{entry.data.title}</span>
|
||||
</nav>
|
||||
|
||||
<div class="article-tags">
|
||||
{entry.data.categories?.map((cat: string) => (
|
||||
<span class="article-tag article-tag-category">{cat}</span>
|
||||
))}
|
||||
{entry.data.tags?.map((tag: string) => (
|
||||
<span class="article-tag">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="article-title-section">
|
||||
<h1 class="article-title">
|
||||
{displayTool?.icon && <span class="article-icon">{displayTool.icon}</span>}
|
||||
{entry.data.title}
|
||||
</h1>
|
||||
<p style="margin: 0; color: var(--color-text-secondary); font-size: 1.125rem;">
|
||||
{entry.data.description}
|
||||
</p>
|
||||
<p class="article-description">{entry.data.description}</p>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 0.5rem; align-items: end;">
|
||||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||
{isStandalone ? (
|
||||
<span class="badge" style="background-color: var(--color-accent); color: white;">Artikel</span>
|
||||
) : (
|
||||
<>
|
||||
{isConcept && <span class="badge" style="background-color: var(--color-concept); color: white;">Konzept</span>}
|
||||
{isMethod && <span class="badge" style="background-color: var(--color-method); color: white;">Methode</span>}
|
||||
{!isMethod && !isConcept && !isStandalone && <span class="badge" style="background-color: var(--color-primary); color: white;">Software</span>}
|
||||
{!isMethod && !isConcept && hasValidProjectUrl && <span class="badge badge-primary">CC24-Server</span>}
|
||||
{!isMethod && !isConcept && !isStandalone && displayTool?.license !== 'Proprietary' && <span class="badge badge-success">Open Source</span>}
|
||||
</>
|
||||
)}
|
||||
<span class="badge badge-error">📖</span>
|
||||
|
||||
<div class="article-metadata-grid">
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Typ</span>
|
||||
<div class="metadata-badges">
|
||||
{isStandalone ? (
|
||||
<span class="badge badge-accent">Artikel</span>
|
||||
) : (
|
||||
<>
|
||||
{isConcept && <span class="badge badge-concept">Konzept</span>}
|
||||
{isMethod && <span class="badge badge-method">Methode</span>}
|
||||
{!isMethod && !isConcept && <span class="badge badge-primary">Software</span>}
|
||||
{hasValidProjectUrl && <span class="badge badge-primary">CC24-Server</span>}
|
||||
</>
|
||||
)}
|
||||
<span class="badge badge-error">📖</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{entry.data.difficulty && (
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Schwierigkeit</span>
|
||||
<span class="metadata-value">{entry.data.difficulty}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Aktualisiert</span>
|
||||
<span class="metadata-value">{entry.data.last_updated.toLocaleDateString('de-DE')}</span>
|
||||
</div>
|
||||
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Autor</span>
|
||||
<span class="metadata-value">{entry.data.author}</span>
|
||||
</div>
|
||||
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Lesezeit</span>
|
||||
<span class="metadata-value" id="reading-time">~5 min</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="article-actions">
|
||||
<button
|
||||
id="share-article-btn"
|
||||
class="btn btn-secondary btn-sm"
|
||||
class="btn btn-secondary"
|
||||
onclick="window.shareCurrentArticle(this)"
|
||||
title="Artikel teilen"
|
||||
style="font-size: 0.8125rem; padding: 0.5rem 0.75rem;"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.375rem;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="18" cy="5" r="3"/>
|
||||
<circle cx="6" cy="12" r="3"/>
|
||||
<circle cx="18" cy="19" r="3"/>
|
||||
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/>
|
||||
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
|
||||
</svg>
|
||||
Artikel teilen
|
||||
Teilen
|
||||
</button>
|
||||
|
||||
<a href="/knowledgebase" class="btn btn-secondary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="15,18 9,12 15,6"></polyline>
|
||||
</svg>
|
||||
Zurück
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--color-border);">
|
||||
{entry.data.difficulty && (
|
||||
<div>
|
||||
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Schwierigkeit</strong>
|
||||
<p style="margin: 0; font-size: 0.9375rem;">{entry.data.difficulty}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Letztes Update</strong>
|
||||
<p style="margin: 0; font-size: 0.9375rem;">{entry.data.last_updated.toLocaleDateString('de-DE')}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Autor</strong>
|
||||
<p style="margin: 0; font-size: 0.9375rem;">{entry.data.author}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Typ</strong>
|
||||
<p style="margin: 0; font-size: 0.9375rem;">
|
||||
{isStandalone ? 'Allgemeiner Artikel' :
|
||||
isConcept ? 'Konzept-Artikel' :
|
||||
isMethod ? 'Methoden-Artikel' :
|
||||
'Software-Artikel'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{entry.data.categories && entry.data.categories.length > 0 && (
|
||||
<div style="grid-column: 1 / -1;">
|
||||
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Kategorien</strong>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem; margin-top: 0.25rem;">
|
||||
{entry.data.categories.map((cat: string) => (
|
||||
<span class="tag" style="font-size: 0.75rem;">{cat}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav style="margin-bottom: 2rem; position: relative; z-index: 50;">
|
||||
<a href="/knowledgebase" class="btn btn-secondary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
||||
<polyline points="15,18 9,12 15,6"></polyline>
|
||||
</svg>
|
||||
Zurück zur Knowledgebase
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="card" style="padding: 2rem;">
|
||||
<div class="kb-content markdown-content" style="line-height: 1.7;">
|
||||
<Content />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top: 2rem; background-color: var(--color-bg-secondary);">
|
||||
<h3 style="margin: 0 0 1rem 0; color: var(--color-text);">
|
||||
{isStandalone ? 'Verwandte Aktionen' : 'Tool-Aktionen'}
|
||||
</h3>
|
||||
<!-- Main Content Area -->
|
||||
<div class="article-content-wrapper">
|
||||
<!-- Sidebar Navigation (will be populated by JS) -->
|
||||
<aside class="article-sidebar">
|
||||
<!-- TOC will be inserted here by JavaScript -->
|
||||
</aside>
|
||||
|
||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
||||
{isStandalone ? (
|
||||
<a href="/knowledgebase" class="btn btn-primary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
<line x1="16" y1="13" x2="8" y2="13"/>
|
||||
<line x1="16" y1="17" x2="8" y2="17"/>
|
||||
<polyline points="10 9 9 9 8 9"/>
|
||||
</svg>
|
||||
Weitere Artikel
|
||||
</a>
|
||||
) : (
|
||||
<>
|
||||
{isConcept ? (
|
||||
<a href={displayTool.url} target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="background-color: var(--color-concept); border-color: var(--color-concept);">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||
<polyline points="15 3 21 3 21 9"/>
|
||||
<line x1="10" y1="14" x2="21" y2="3"/>
|
||||
</svg>
|
||||
Mehr erfahren
|
||||
</a>
|
||||
) : isMethod ? (
|
||||
<a href={displayTool.projectUrl || displayTool.url} target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="background-color: var(--color-method); border-color: var(--color-method);">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||
<polyline points="15 3 21 3 21 9"/>
|
||||
<line x1="10" y1="14" x2="21" y2="3"/>
|
||||
</svg>
|
||||
Zur Methode
|
||||
</a>
|
||||
) : (
|
||||
<>
|
||||
<a href={displayTool.url} target="_blank" rel="noopener noreferrer" class="btn btn-secondary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||
<polyline points="15 3 21 3 21 9"/>
|
||||
<line x1="10" y1="14" x2="21" y2="3"/>
|
||||
</svg>
|
||||
Software-Homepage
|
||||
</a>
|
||||
{hasValidProjectUrl && (
|
||||
<a href={displayTool.projectUrl} target="_blank" rel="noopener noreferrer" class="btn btn-primary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<path d="M12 16l4-4-4-4"/>
|
||||
<path d="M8 12h8"/>
|
||||
</svg>
|
||||
Zugreifen
|
||||
</a>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<!-- Article Content -->
|
||||
<main class="article-main">
|
||||
<article class="article-content">
|
||||
<div class="markdown-content">
|
||||
<Content />
|
||||
</div>
|
||||
</article>
|
||||
|
||||
{relatedTools.length > 0 && relatedTools.length > (primaryTool ? 1 : 0) && (
|
||||
<div style="margin-left: auto;">
|
||||
<details style="position: relative;">
|
||||
<summary class="btn btn-secondary" style="cursor: pointer; list-style: none;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="8.5" cy="7" r="4"/>
|
||||
<line x1="20" y1="8" x2="20" y2="14"/>
|
||||
<line x1="23" y1="11" x2="17" y2="11"/>
|
||||
<!-- Article Footer -->
|
||||
<footer class="article-footer">
|
||||
<div class="article-footer-actions">
|
||||
<h3>Tool-Aktionen</h3>
|
||||
<div class="footer-actions-grid">
|
||||
{isStandalone ? (
|
||||
<a href="/knowledgebase" class="btn btn-primary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
</svg>
|
||||
Weitere Artikel
|
||||
</a>
|
||||
) : (
|
||||
<>
|
||||
{isConcept ? (
|
||||
<a href={displayTool.url} target="_blank" rel="noopener noreferrer" class="btn btn-concept">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||
<polyline points="15 3 21 3 21 9"/>
|
||||
<line x1="10" y1="14" x2="21" y2="3"/>
|
||||
</svg>
|
||||
Mehr erfahren
|
||||
</a>
|
||||
) : isMethod ? (
|
||||
<a href={displayTool.projectUrl || displayTool.url} target="_blank" rel="noopener noreferrer" class="btn btn-method">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||
<polyline points="15 3 21 3 21 9"/>
|
||||
<line x1="10" y1="14" x2="21" y2="3"/>
|
||||
</svg>
|
||||
Zur Methode
|
||||
</a>
|
||||
) : (
|
||||
<>
|
||||
<a href={displayTool.url} target="_blank" rel="noopener noreferrer" class="btn btn-secondary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||
<polyline points="15 3 21 3 21 9"/>
|
||||
<line x1="10" y1="14" x2="21" y2="3"/>
|
||||
</svg>
|
||||
Homepage
|
||||
</a>
|
||||
{hasValidProjectUrl && (
|
||||
<a href={displayTool.projectUrl} target="_blank" rel="noopener noreferrer" class="btn btn-primary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<path d="M12 16l4-4-4-4"/>
|
||||
<path d="M8 12h8"/>
|
||||
</svg>
|
||||
Zugreifen
|
||||
</a>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<a href="/" class="btn btn-secondary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
|
||||
<polyline points="9,22 9,12 15,12 15,22"/>
|
||||
</svg>
|
||||
Verwandte Tools ({relatedTools.length})
|
||||
</summary>
|
||||
<div style="position: absolute; top: 100%; left: 0; background: var(--color-bg); border: 1px solid var(--color-border); border-radius: 0.5rem; padding: 1rem; min-width: 200px; z-index: 100; box-shadow: var(--shadow-lg);">
|
||||
Zur Hauptseite
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{relatedTools.length > 0 && (
|
||||
<div class="related-tools">
|
||||
<h3>Verwandte Tools</h3>
|
||||
<div class="related-tools-grid">
|
||||
{relatedTools.map((tool: any) => (
|
||||
<a href={tool.projectUrl || tool.url} target="_blank" rel="noopener noreferrer"
|
||||
style="display: block; padding: 0.5rem; border-radius: 0.25rem; text-decoration: none; color: var(--color-text); margin-bottom: 0.25rem;"
|
||||
onmouseover="this.style.backgroundColor='var(--color-bg-secondary)'"
|
||||
onmouseout="this.style.backgroundColor='transparent'">
|
||||
{tool.icon && <span style="margin-right: 0.5rem;">{tool.icon}</span>}
|
||||
{tool.name}
|
||||
<a href={tool.projectUrl || tool.url} target="_blank" rel="noopener noreferrer" class="related-tool-card">
|
||||
{tool.icon && <span class="tool-icon">{tool.icon}</span>}
|
||||
<span class="tool-name">{tool.name}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<a href="/" class="btn btn-secondary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
|
||||
<polyline points="9,22 9,12 15,12 15,22"/>
|
||||
</svg>
|
||||
Zur Hauptseite
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/** @template {Element} T
|
||||
* @param {string} sel
|
||||
* @param {Document|Element} [root=document]
|
||||
* @returns {T|null} */
|
||||
const qs = (sel, root = document) => root.querySelector(sel);
|
||||
|
||||
/** @template {Element} T
|
||||
* @param {string} sel
|
||||
* @param {Document|Element} [root=document]
|
||||
* @returns {T[]} */
|
||||
const qsa = (sel, root = document) => Array.from(root.querySelectorAll(sel));
|
||||
|
||||
function calculateReadingTime() {
|
||||
/** @type {HTMLElement|null} */
|
||||
const content = qs('.markdown-content');
|
||||
if (!content) return;
|
||||
|
||||
const text = (content.textContent || content.innerText || '').trim();
|
||||
if (!text) return;
|
||||
|
||||
const wordsPerMinute = 200;
|
||||
const words = text.split(/\s+/).length;
|
||||
const readingTime = Math.ceil(words / wordsPerMinute);
|
||||
|
||||
const readingTimeElement = document.getElementById('reading-time');
|
||||
if (readingTimeElement) {
|
||||
readingTimeElement.textContent = `~${readingTime} min`;
|
||||
}
|
||||
}
|
||||
|
||||
function generateSidebarTOC() {
|
||||
/** @type {HTMLElement|null} */
|
||||
const article = qs('.markdown-content');
|
||||
/** @type {HTMLElement|null} */
|
||||
const sidebar = qs('.article-sidebar');
|
||||
/** @type {HTMLElement|null} */
|
||||
const main = qs('.article-main');
|
||||
|
||||
if (!article || !sidebar || !main) return;
|
||||
|
||||
/** @type {HTMLHeadingElement[]} */
|
||||
const headings = qsa('h1, h2, h3, h4, h5, h6', article);
|
||||
|
||||
if (headings.length < 2) {
|
||||
sidebar.style.display = 'none';
|
||||
main.style.maxWidth = '100%';
|
||||
return;
|
||||
}
|
||||
|
||||
headings.forEach((h, i) => {
|
||||
if (!h.id) h.id = `heading-${i}`;
|
||||
});
|
||||
|
||||
const tocHTML = `
|
||||
<div class="sidebar-toc">
|
||||
<h3 class="toc-title">Inhaltsverzeichnis</h3>
|
||||
<nav class="toc-navigation">
|
||||
${headings.map((h) => {
|
||||
const level = parseInt(h.tagName.slice(1), 10);
|
||||
const text = (h.textContent || '').trim();
|
||||
const id = h.id;
|
||||
return `
|
||||
<a href="#${id}" class="toc-item toc-level-${level}" data-heading="${id}">
|
||||
${text}
|
||||
</a>
|
||||
`;
|
||||
}).join('')}
|
||||
</nav>
|
||||
</div>
|
||||
`;
|
||||
|
||||
sidebar.innerHTML = tocHTML;
|
||||
|
||||
/** @type {HTMLAnchorElement[]} */
|
||||
const tocItems = qsa('.toc-item', sidebar);
|
||||
|
||||
tocItems.forEach((item) => {
|
||||
item.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const href = item.getAttribute('href');
|
||||
if (!href || !href.startsWith('#')) return;
|
||||
|
||||
const target = document.getElementById(href.slice(1));
|
||||
if (target) {
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' });
|
||||
tocItems.forEach((i) => i.classList.remove('active'));
|
||||
item.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const updateActiveSection = () => {
|
||||
const scrollY = window.scrollY + 100;
|
||||
let currentId = null;
|
||||
|
||||
for (let i = 0; i < headings.length; i++) {
|
||||
const h = headings[i];
|
||||
const rect = h.getBoundingClientRect();
|
||||
const absTop = rect.top + window.pageYOffset;
|
||||
if (absTop <= scrollY) currentId = h.id;
|
||||
}
|
||||
|
||||
if (currentId) {
|
||||
tocItems.forEach((i) => i.classList.remove('active'));
|
||||
/** @type {HTMLAnchorElement|null} */
|
||||
const active = qs(`.toc-item[data-heading="${currentId}"]`, sidebar);
|
||||
if (active) active.classList.add('active');
|
||||
}
|
||||
};
|
||||
|
||||
let ticking = false;
|
||||
window.addEventListener('scroll', () => {
|
||||
if (ticking) return;
|
||||
ticking = true;
|
||||
requestAnimationFrame(() => {
|
||||
updateActiveSection();
|
||||
ticking = false;
|
||||
});
|
||||
});
|
||||
|
||||
updateActiveSection();
|
||||
}
|
||||
|
||||
function enhanceCodeCopy() {
|
||||
/** @type {HTMLPreElement[]} */
|
||||
const pres = qsa('.markdown-content pre');
|
||||
|
||||
pres.forEach((pre) => {
|
||||
if (pre.dataset.copyEnhanced === 'true') return;
|
||||
pre.dataset.copyEnhanced = 'true';
|
||||
pre.style.position ||= 'relative';
|
||||
|
||||
// Try to find an existing copy button we can reuse
|
||||
let btn =
|
||||
pre.querySelector('.copy-btn') || // our class
|
||||
pre.querySelector('.btn-copy, .copy-button, .code-copy, .copy-code, button[aria-label*="copy" i]');
|
||||
|
||||
// If there is an "old" button that is NOT ours, prefer to reuse it by giving it our class.
|
||||
if (btn && !btn.classList.contains('copy-btn')) {
|
||||
btn.classList.add('copy-btn');
|
||||
}
|
||||
|
||||
// If no button at all, create one
|
||||
if (!btn) {
|
||||
btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'copy-btn';
|
||||
btn.setAttribute('aria-label', 'Code kopieren');
|
||||
btn.innerHTML = `
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2"/>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
||||
<span>Copy</span>
|
||||
`;
|
||||
pre.appendChild(btn);
|
||||
}
|
||||
|
||||
// If there is a SECOND old button lingering (top-left in your case), hide it
|
||||
const possibleOldButtons = pre.querySelectorAll(
|
||||
'.btn-copy, .copy-button, .code-copy, .copy-code, button[aria-label*="copy" i]'
|
||||
);
|
||||
possibleOldButtons.forEach((b) => {
|
||||
if (b !== btn) b.style.display = 'none';
|
||||
});
|
||||
|
||||
// Success pill
|
||||
if (!pre.querySelector('.copied-pill')) {
|
||||
const pill = document.createElement('div');
|
||||
pill.className = 'copied-pill';
|
||||
pill.textContent = '✓ Kopiert';
|
||||
pre.appendChild(pill);
|
||||
}
|
||||
|
||||
// Screen reader live region
|
||||
if (!pre.querySelector('.sr-live')) {
|
||||
const live = document.createElement('div');
|
||||
live.className = 'sr-live';
|
||||
live.setAttribute('aria-live', 'polite');
|
||||
Object.assign(live.style, {
|
||||
position: 'absolute', width: '1px', height: '1px', padding: '0', margin: '-1px',
|
||||
overflow: 'hidden', clip: 'rect(0,0,0,0)', border: '0'
|
||||
});
|
||||
pre.appendChild(live);
|
||||
}
|
||||
|
||||
btn.addEventListener('click', async () => {
|
||||
const code = pre.querySelector('code');
|
||||
const text = code ? code.innerText : pre.innerText;
|
||||
const live = pre.querySelector('.sr-live');
|
||||
|
||||
const copyText = async (t) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(t);
|
||||
return true;
|
||||
} catch {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = t;
|
||||
ta.style.position = 'fixed';
|
||||
ta.style.top = '-1000px';
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
const ok = document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
return ok;
|
||||
}
|
||||
};
|
||||
|
||||
const ok = await copyText(text);
|
||||
pre.dataset.copied = ok ? 'true' : 'false';
|
||||
if (live) live.textContent = ok ? 'Code in die Zwischenablage kopiert' : 'Kopieren fehlgeschlagen';
|
||||
|
||||
window.setTimeout(() => { pre.dataset.copied = 'false'; }, 1200);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// keep your existing DOMContentLoaded; just ensure this is called
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// existing:
|
||||
calculateReadingTime();
|
||||
generateSidebarTOC();
|
||||
// new/updated:
|
||||
enhanceCodeCopy();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
</BaseLayout>
|
||||
Reference in New Issue
Block a user