knowledgebase style

This commit is contained in:
overcuriousity
2025-08-10 19:57:59 +02:00
parent 9a3122745d
commit a816c0630f
5 changed files with 1124 additions and 342 deletions

View File

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