Downloaded theme

This commit is contained in:
2026-03-23 18:35:59 +01:00
parent 326fab0e42
commit d091efd432
86 changed files with 14512 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
<div class="md-alert md-alert-note">
<div class="md-mermaid">
<pre class="mermaid">
{{ .Text | safeHTML }}
</pre>
</div>
</div>

View File

@@ -0,0 +1,10 @@
{{- /* Check if this is a GitHub-style alert */ -}}
{{- if eq .Type "alert" -}}
<div class="md-alert md-alert-{{ .AlertType }}">
{{ .Text | safeHTML }}
</div>
{{- else -}}
<blockquote class="md-blockquote">
{{ .Text | safeHTML }}
</blockquote>
{{- end -}}

View File

@@ -0,0 +1,5 @@
<div class="md-mermaid">
<pre class="mermaid">
{{ .Inner | safeHTML }}
</pre>
</div>

View File

@@ -0,0 +1,337 @@
{{- $lang := .Type | default "text" -}}
{{- $filename := .Attributes.filename | default "" -}}
{{- $label := cond (ne $filename "") $filename ($lang | upper) -}}
{{- $id := printf "cb-%s" (printf "%d" .Ordinal | sha256 | truncate 8 "") -}}
{{- $collapseEnabled := site.Params.codeblock.collapse.enabled | default true -}}
{{- $defaultState := site.Params.codeblock.collapse.defaultState | default "expanded" -}}
{{- $collapsedHeight := site.Params.codeblock.collapse.collapsedHeight | default 200 -}}
{{- $highlighted := transform.HighlightCodeBlock . -}}
<div class="mb-codeblock" id="{{ $id }}" data-lang="{{ $lang | lower }}">
<!-- Header with language badge and actions -->
<div class="mb-codeblock-header">
<div class="mb-codeblock-left">
<span class="mb-codeblock-badge">
{{ $lang | upper }}
</span>
{{- if ne $filename "" -}}
<span class="mb-codeblock-filename">
<i class="fas fa-file-code" style="font-size: 0.65rem;"></i>
{{ $filename }}
</span>
{{- end -}}
</div>
<div class="mb-codeblock-actions">
{{- if $collapseEnabled -}}
<button class="mb-action-btn mb-collapse-btn" data-collapsed="false" aria-label="Collapse code">
<i class="fas fa-compress-alt"></i>
<span>Collapse</span>
</button>
{{- end -}}
<button class="mb-action-btn mb-copy-btn" aria-label="Copy code">
<i class="fas fa-copy"></i>
<span>Copy</span>
</button>
</div>
</div>
<!-- Code content -->
<div
class="mb-codeblock-content"
data-state="{{ $defaultState }}"
{{- if eq $defaultState "collapsed" }}
style="max-height: {{ $collapsedHeight }}px; overflow: hidden;"
{{- end }}
>
{{ $highlighted.Wrapped }}
{{- if $collapseEnabled -}}
<div class="mb-collapse-overlay">
<button class="mb-expand-trigger">
<i class="fas fa-chevron-down"></i>
<span>Click to expand</span>
</button>
</div>
{{- end -}}
</div>
</div>
<script>
(function() {
'use strict';
const codeblock = document.getElementById('{{ $id }}');
if (!codeblock) return;
const copyBtn = codeblock.querySelector('.mb-copy-btn');
const collapseBtn = codeblock.querySelector('.mb-collapse-btn');
const content = codeblock.querySelector('.mb-codeblock-content');
const overlay = codeblock.querySelector('.mb-collapse-overlay');
const expandTrigger = overlay?.querySelector('.mb-expand-trigger');
// ==================
// COPY FUNCTIONALITY
// ==================
if (copyBtn) {
copyBtn.addEventListener('click', async function() {
let codeText = '';
// Handle line-numbered code (Hugo's table format)
const codeCell = codeblock.querySelector('.lntd:last-child code');
if (codeCell) {
codeText = codeCell.textContent || codeCell.innerText || '';
} else {
// Regular code block
const codeEl = codeblock.querySelector('pre code');
codeText = codeEl ? (codeEl.textContent || codeEl.innerText || '') : '';
}
try {
await navigator.clipboard.writeText(codeText.trim());
// Success feedback
const originalHTML = copyBtn.innerHTML;
copyBtn.innerHTML = '<i class="fas fa-check"></i><span>Copied!</span>';
copyBtn.classList.add('mb-btn-success');
setTimeout(() => {
copyBtn.innerHTML = originalHTML;
copyBtn.classList.remove('mb-btn-success');
}, 2000);
} catch (err) {
console.error('Failed to copy code:', err);
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = codeText.trim();
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
copyBtn.innerHTML = '<i class="fas fa-check"></i><span>Copied!</span>';
setTimeout(() => {
copyBtn.innerHTML = '<i class="fas fa-copy"></i><span>Copy</span>';
}, 2000);
} catch (fallbackErr) {
console.error('Fallback copy failed:', fallbackErr);
}
document.body.removeChild(textArea);
}
});
}
// =======================
// COLLAPSE FUNCTIONALITY
// =======================
if (collapseBtn && content && overlay) {
const collapsedHeight = {{ $collapsedHeight }};
function toggleCollapse() {
const isCollapsed = content.dataset.state === 'collapsed';
if (isCollapsed) {
// EXPAND
content.style.maxHeight = '';
content.style.overflow = '';
content.dataset.state = 'expanded';
overlay.style.display = 'none';
collapseBtn.innerHTML = '<i class="fas fa-compress-alt"></i><span>Collapse</span>';
collapseBtn.dataset.collapsed = 'false';
} else {
// COLLAPSE
content.style.maxHeight = collapsedHeight + 'px';
content.style.overflow = 'hidden';
content.dataset.state = 'collapsed';
overlay.style.display = 'flex';
collapseBtn.innerHTML = '<i class="fas fa-expand-alt"></i><span>Expand</span>';
collapseBtn.dataset.collapsed = 'true';
}
}
// Initialize if default state is collapsed
if ('{{ $defaultState }}' === 'collapsed') {
content.dataset.state = 'collapsed';
overlay.style.display = 'flex';
collapseBtn.innerHTML = '<i class="fas fa-expand-alt"></i><span>Expand</span>';
collapseBtn.dataset.collapsed = 'true';
}
collapseBtn.addEventListener('click', toggleCollapse);
if (expandTrigger) {
expandTrigger.addEventListener('click', toggleCollapse);
}
}
})();
</script>
<style>
/* Additional styles for improved codeblock - add to your main.css */
.mb-codeblock-filename {
display: inline-flex;
align-items: center;
gap: 0.35rem;
color: var(--color-text);
font-size: 0.75rem;
font-weight: 500;
padding: 0.2rem 0.6rem;
border-radius: 0.35rem;
background: color-mix(in srgb, var(--color-bg) 40%, transparent);
border: 1px solid color-mix(in srgb, var(--color-border) 60%, transparent);
}
.mb-action-btn {
display: inline-flex;
align-items: center;
gap: 0.35rem;
background: transparent;
border: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent);
color: var(--color-text-muted);
cursor: pointer;
font-size: 0.7rem;
padding: 0.35rem 0.65rem;
border-radius: 0.4rem;
transition: all 0.15s ease-out;
font-family: inherit;
}
.mb-action-btn:hover {
color: var(--color-accent);
background: color-mix(in srgb, var(--color-accent) 12%, transparent);
border-color: var(--color-accent);
transform: translateY(-1px);
}
.mb-action-btn:active {
transform: translateY(0);
}
.mb-action-btn i {
font-size: 0.7rem;
}
.mb-btn-success {
color: #22c55e !important;
border-color: #22c55e !important;
background: color-mix(in srgb, #22c55e 12%, transparent) !important;
}
.mb-collapse-overlay {
display: none;
position: absolute;
inset: 0;
background: linear-gradient(
to bottom,
transparent 0%,
rgba(0, 0, 0, 0.3) 40%,
rgba(0, 0, 0, 0.85) 100%
);
align-items: flex-end;
justify-content: center;
padding-bottom: 1rem;
cursor: pointer;
z-index: 10;
}
.mb-expand-trigger {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.4rem 0.8rem;
border-radius: 0.5rem;
border: 1px solid var(--color-accent);
background: color-mix(in srgb, var(--color-accent) 20%, transparent);
color: var(--color-accent);
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
transition: all 0.15s ease-out;
backdrop-filter: blur(8px);
}
.mb-expand-trigger:hover {
background: color-mix(in srgb, var(--color-accent) 30%, transparent);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(168, 85, 247, 0.3);
}
.mb-expand-trigger i {
font-size: 0.7rem;
animation: bounce 1.5s infinite;
}
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(3px);
}
}
/* Language-specific badge colors */
.mb-codeblock[data-lang="javascript"] .mb-codeblock-badge,
.mb-codeblock[data-lang="js"] .mb-codeblock-badge {
background: color-mix(in srgb, #f7df1e 25%, transparent);
color: #f7df1e;
}
.mb-codeblock[data-lang="typescript"] .mb-codeblock-badge,
.mb-codeblock[data-lang="ts"] .mb-codeblock-badge {
background: color-mix(in srgb, #3178c6 25%, transparent);
color: #3178c6;
}
.mb-codeblock[data-lang="python"] .mb-codeblock-badge,
.mb-codeblock[data-lang="py"] .mb-codeblock-badge {
background: color-mix(in srgb, #3776ab 25%, transparent);
color: #3776ab;
}
.mb-codeblock[data-lang="go"] .mb-codeblock-badge {
background: color-mix(in srgb, #00add8 25%, transparent);
color: #00add8;
}
.mb-codeblock[data-lang="rust"] .mb-codeblock-badge,
.mb-codeblock[data-lang="rs"] .mb-codeblock-badge {
background: color-mix(in srgb, #ce422b 25%, transparent);
color: #ce422b;
}
.mb-codeblock[data-lang="html"] .mb-codeblock-badge {
background: color-mix(in srgb, #e34c26 25%, transparent);
color: #e34c26;
}
.mb-codeblock[data-lang="css"] .mb-codeblock-badge {
background: color-mix(in srgb, #264de4 25%, transparent);
color: #264de4;
}
.mb-codeblock[data-lang="bash"] .mb-codeblock-badge,
.mb-codeblock[data-lang="sh"] .mb-codeblock-badge,
.mb-codeblock[data-lang="shell"] .mb-codeblock-badge {
background: color-mix(in srgb, #4eaa25 25%, transparent);
color: #4eaa25;
}
.mb-codeblock[data-lang="json"] .mb-codeblock-badge {
background: color-mix(in srgb, #000000 25%, transparent);
color: #dddddd;
}
.mb-codeblock[data-lang="yaml"] .mb-codeblock-badge,
.mb-codeblock[data-lang="yml"] .mb-codeblock-badge {
background: color-mix(in srgb, #cb171e 25%, transparent);
color: #cb171e;
}
</style>

View File

@@ -0,0 +1,5 @@
<h{{ .Level }} id="{{ .Anchor }}">
<a href="#{{ .Anchor }}" class="md-heading-anchor">
{{ .Text | safeHTML }}
</a>
</h{{ .Level }}>

View File

@@ -0,0 +1,12 @@
<figure class="md-image">
<a href="{{ .Destination | safeURL }}" class="glightbox">
<img
src="{{ .Destination | safeURL }}"
alt="{{ .Text }}"
loading="lazy"
/>
</a>
{{ with .Title }}
<figcaption>{{ . }}</figcaption>
{{ end }}
</figure>

View File

@@ -0,0 +1,7 @@
<a
href="{{ .Destination | safeURL }}"
{{ if strings.HasPrefix .Destination "http" }}target="_blank" rel="noopener"{{ end }}
class="md-link"
>
{{ .Text | safeHTML }}
</a>

View File

@@ -0,0 +1,39 @@
<div class="table-wrap">
<table
{{- range $k, $v := .Attributes }}
{{- if $v }}
{{- printf " %s=%q" $k $v | safeHTMLAttr }}
{{- end }}
{{- end }}>
<thead>
{{- range .THead }}
<tr>
{{- range . }}
<th
{{- with .Alignment }}
{{- printf " style=%q" (printf "text-align: %s" .) | safeHTMLAttr }}
{{- end -}}
>
{{- .Text -}}
</th>
{{- end }}
</tr>
{{- end }}
</thead>
<tbody>
{{- range .TBody }}
<tr>
{{- range . }}
<td
{{- with .Alignment }}
{{- printf " style=%q" (printf "text-align: %s" .) | safeHTMLAttr }}
{{- end -}}
>
{{- .Text -}}
</td>
{{- end }}
</tr>
{{- end }}
</tbody>
</table>
</div>

View File

@@ -0,0 +1,4 @@
{{- $type := .Get "type" | default "note" -}}
<div class="md-alert md-alert-{{ $type }}">
{{ .Inner | markdownify }}
</div>

View File

@@ -0,0 +1,35 @@
{{- $images := split (.Get "images") "," -}}
{{- $captions := split (default "" (.Get "captions")) "," -}}
<div class="gallery-container" data-jg>
{{- if .Inner -}}
{{- /* If Inner content is provided, convert markdown images to lightbox-ready anchors */ -}}
{{- $content := .Inner -}}
{{- $content = replaceRE `!\[([^\]]*)\]\(([^\)]+)\)` `<a href="$2" class="glightbox" data-glightbox="description: $1"><img src="$2" alt="$1" loading="lazy"></a>` $content -}}
{{ $content | safeHTML }}
{{- else if $images -}}
{{- /* Otherwise, generate from images parameter */ -}}
{{- range $index, $image := $images -}}
{{- $imagePath := trim $image " " -}}
{{- $caption := "" -}}
{{- if $captions -}}
{{- $caption = index $captions $index | default "" | trim " " -}}
{{- end -}}
{{- if $imagePath -}}
<a href="{{ $imagePath | relURL }}" class="glightbox" data-glightbox="{{ if $caption }}description: {{ $caption }}{{ end }}">
<img src="{{ $imagePath | relURL }}" alt="{{ $caption | default $imagePath }}" loading="lazy">
</a>
{{- end -}}
{{- end -}}
{{- else -}}
{{- /* Fallback: try to get images from page resources */ -}}
{{- $resources := .Page.Resources.Match "images/*" -}}
{{- if $resources -}}
{{- range $resources -}}
<a href="{{ .RelPermalink }}" class="glightbox">
<img src="{{ .Resize "300x" }}" alt="{{ .Name }}" loading="lazy">
</a>
{{- end -}}
{{- end -}}
{{- end -}}
</div>

View File

@@ -0,0 +1,156 @@
{{ define "main" }}
<section class="layout-page">
<article class="about-alt-page">
<div class="about-alt-layout">
<!-- Left Sidebar - Profile Card -->
<aside class="about-alt-sidebar">
<div class="about-alt-profile-card">
{{ with .Site.Params.hero.avatar }}
<div class="about-alt-avatar">
<img src="{{ . | relURL }}" alt="{{ $.Site.Params.brand }}" />
</div>
{{ else }}
<div class="about-alt-avatar-placeholder">
<i class="fas fa-code"></i>
</div>
{{ end }}
<h1 class="about-alt-name">{{ $.Site.Params.brand }}</h1>
{{ with .Params.subtitle }}
<p class="about-alt-role">{{ . }}</p>
{{ end }}
{{ with .Site.Params.hero.location }}
<div class="about-alt-meta">
<i class="fas fa-map-marker-alt"></i>
<span>{{ . }}</span>
</div>
{{ end }}
<!-- Quick Stats -->
{{ with $.Site.Params.about.alt.stats }}
<div class="about-alt-stats">
{{ range . }}
<div class="about-alt-stat">
<div class="about-alt-stat-value">{{ .value }}</div>
<div class="about-alt-stat-label">{{ .label }}</div>
</div>
{{ end }}
</div>
{{ end }}
<!-- Social Links -->
{{ with .Site.Params.social }}
<div class="about-alt-social">
{{ range . }}
<a
href="{{ .url }}"
class="about-alt-social-icon"
target="_blank"
rel="noopener noreferrer"
aria-label="{{ .label }}"
>
<i class="{{ .icon }}"></i>
</a>
{{ end }}
</div>
{{ end }}
</div>
</aside>
<!-- Right Content Area -->
<div class="about-alt-content">
<!-- Introduction -->
<div class="about-alt-section">
<div class="markdown-body">
{{ $content := .Content }}
{{ $content = replace $content "<hr>" "|||SPLIT|||" }}
{{ $content = replace $content "<hr />" "|||SPLIT|||" }}
{{ $content = replace $content "<hr/>" "|||SPLIT|||" }}
{{ $parts := split $content "|||SPLIT|||" }}
{{ index $parts 0 | safeHTML }}
</div>
</div>
<!-- Experience Cards -->
{{ if gt (len $parts) 1 }}
{{- $experienceParts := slice -}}
{{- $additionalContent := "" -}}
{{- $foundNonExperience := false -}}
{{- range $index, $part := after 1 $parts -}}
{{- $trimmed := trim $part " \n\t" -}}
{{- if $trimmed -}}
{{- /* Check if this looks like an experience card (starts with <p><strong>) or is additional content (starts with <h) */ -}}
{{- if or (hasPrefix $trimmed "<p><strong>") (hasPrefix $trimmed "<p><strong ") (hasPrefix $trimmed "<p>\n<strong") (hasPrefix $trimmed "<p> <strong") -}}
{{- if not $foundNonExperience -}}
{{- $experienceParts = $experienceParts | append $part -}}
{{- else -}}
{{- $additionalContent = printf "%s|||SPLIT|||%s" $additionalContent $part -}}
{{- end -}}
{{- else -}}
{{- $foundNonExperience = true -}}
{{- $additionalContent = printf "%s|||SPLIT|||%s" $additionalContent $part -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- if gt (len $experienceParts) 0 -}}
<div class="about-alt-section">
<h2 class="about-alt-section-title">
<i class="fas fa-briefcase"></i>
<span>Experience</span>
</h2>
<div class="about-alt-experience-grid">
{{- range $experienceParts -}}
<div class="about-alt-experience-card">
{{ . | safeHTML }}
</div>
{{- end -}}
</div>
</div>
{{- end -}}
{{- /* Render additional content sections */ -}}
{{- if $additionalContent -}}
{{- $additionalParts := split $additionalContent "|||SPLIT|||" -}}
{{- range $additionalParts -}}
{{- $trimmed := trim . " \n\t" -}}
{{- if $trimmed -}}
<div class="about-alt-section">
<div class="markdown-body">
{{ . | safeHTML }}
</div>
</div>
{{- end -}}
{{- end -}}
{{- end -}}
{{ end }}
<!-- Skills Badge Cloud -->
{{ with $.Site.Params.about.alt.skills }}
<div class="about-alt-section">
<h2 class="about-alt-section-title">
<i class="fas fa-code"></i>
<span>Tech Stack</span>
</h2>
<div class="about-alt-skills">
{{ range . }}
<span class="about-alt-skill">
{{ with .icon }}<i class="{{ . }}"></i>{{ end }}
{{ .label }}
</span>
{{ end }}
</div>
</div>
{{ end }}
</div>
</div>
</article>
</section>
{{ end }}

View File

@@ -0,0 +1,77 @@
{{ define "main" }}
<section class="layout-page">
<article class="about-page">
<!-- Hero Section -->
<header class="about-hero">
<div class="about-hero-content">
{{ with .Site.Params.hero.avatar }}
<div class="about-avatar">
<img src="{{ . | relURL }}" alt="{{ $.Site.Params.brand }}" />
</div>
{{ end }}
<h1 class="about-title">{{ .Title }}</h1>
{{ with .Params.subtitle }}
<p class="about-subtitle">{{ . }}</p>
{{ end }}
</div>
</header>
<!-- Main Content -->
<div class="about-content">
<div class="card card-pad markdown-body">
{{ $content := .Content }}
{{ $content = replace $content "<hr>" "|||SPLIT|||" }}
{{ $content = replace $content "<hr />" "|||SPLIT|||" }}
{{ $content = replace $content "<hr/>" "|||SPLIT|||" }}
{{ $parts := split $content "|||SPLIT|||" }}
{{ if eq (len $parts) 1 }}
<!-- No timeline, render normally -->
{{ .Content }}
{{ else }}
<!-- Render intro section (everything before first hr) -->
{{ index $parts 0 | safeHTML }}
<!-- Render timeline -->
<div class="timeline">
{{ range after 1 $parts }}
{{ $trimmed := trim . " \n\t" }}
{{ if $trimmed }}
<div class="timeline-item">
<div class="timeline-marker"></div>
<div class="timeline-content">
{{ . | safeHTML }}
</div>
</div>
{{ end }}
{{ end }}
</div>
{{ end }}
</div>
</div>
<!-- Social Links Footer -->
{{ with .Site.Params.social }}
<div class="about-social">
<h3 class="about-social-title">Let's Connect</h3>
<div class="about-social-links">
{{ range . }}
<a
href="{{ .url }}"
class="about-social-link"
target="_blank"
rel="noopener noreferrer"
aria-label="{{ .label }}"
>
<i class="{{ .icon }}"></i>
<span>{{ .label }}</span>
</a>
{{ end }}
</div>
</div>
{{ end }}
</article>
</section>
{{ end }}

View File

@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="{{ .Site.Language.Lang }}"
data-cb-collapse-enabled="{{ .Site.Params.features.codeblock.collapse.enabled }}"
data-cb-collapse-default="{{ .Site.Params.features.codeblock.collapse.defaultState }}"
data-cb-collapse-lines="{{ .Site.Params.features.codeblock.collapse.autoCollapseLines }}"
data-cb-collapse-auto-height="{{ .Site.Params.features.codeblock.collapse.autoCollapseHeight }}"
data-cb-collapse-collapsed-height="{{ .Site.Params.features.codeblock.collapse.collapsedHeight }}">
<head>
{{ partial "head.html" . }}
</head>
<body class="min-h-screen bg-bg text-text antialiased">
<div class="flex min-h-screen flex-col">
{{ partial "header.html" . }}
<main class="flex-1">
{{ block "main" . }}{{ end }}
</main>
{{ partial "footer.html" . }}
</div>
{{ partial "search-overlay.html" . }}
{{ partial "dock.html" . }}
<script src="{{ "js/main.js" | relURL }}" defer></script>
<script src="{{ "js/search.js" | relURL }}" defer></script>
<script src="https://cdn.jsdelivr.net/npm/glightbox/dist/js/glightbox.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/justified-gallery@3.8.2/dist/js/justifiedGallery.min.js" defer></script>
<script src="{{ "js/lightbox.js" | relURL }}" defer></script>
<script src="{{ "js/gallery.js" | relURL }}" defer></script>
</body>
</html>

View File

@@ -0,0 +1,30 @@
{{ define "main" }}
<section class="layout-page-tight">
<div class="page-int">
<header class="mb-8">
<h1 class="heading-page text-2xl sm:text-3xl">{{ .Title }}</h1>
{{ with .Description }}
<p class="mt-2 text-sm text-muted">{{ . }}</p>
{{ end }}
</header>
<div class="space-y-6">
{{ range .Pages.ByDate.Reverse }}
<article class="border-b border-border pb-6 last:border-0">
<a href="{{ .RelPermalink }}" class="group block">
<h2 class="text-lg font-medium tracking-tight group-hover:text-accent">
{{ .Title }}
</h2>
{{ with .Params.description }}
<p class="mt-1 text-sm text-muted">{{ . }}</p>
{{ end }} {{ with .Date }}
<p class="mt-2 text-xs text-muted">{{ .Format "January 2, 2006" }}</p>
{{ end }}
</a>
</article>
{{ end }}
</div>
</section>
{{ end }}
</div>

View File

@@ -0,0 +1,154 @@
{{ define "main" }}
<section class="layout-page section-stack">
<div class="article-layout">
<article class="article-main space-y-4">
<header class="space-y-2">
<h1 class="heading-page text-2xl sm:text-3xl">{{ .Title }}</h1>
<div class="text-[0.75rem] text-muted">
{{ with .Date }}
<span>{{ .Format "02 Jan 2006" }}</span>
{{ end }}
{{ with .ReadingTime }}
<span> • {{ . }} min read</span>
{{ end }}
</div>
{{ with .Params.description }}
<p class="max-w-xl text-sm text-muted">
{{ . }}
</p>
{{ end }}
</header>
<div class="card card-pad">
<div class="markdown-body">
{{ .Content }}
</div>
</div>
</article>
{{ if .TableOfContents }}
<aside class="article-toc">
<div class="toc-wrapper">
<div class="toc-header">
<h3 class="toc-title">
<i class="fas fa-list-ul"></i>
<span>Table of Contents</span>
</h3>
<button class="toc-toggle" aria-label="Toggle table of contents">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<nav class="toc-nav">
{{ .TableOfContents }}
</nav>
</div>
</aside>
<script>
(function() {
'use strict';
const tocWrapper = document.querySelector('.toc-wrapper');
const tocToggle = document.querySelector('.toc-toggle');
const tocNav = document.querySelector('.toc-nav');
const tocLinks = document.querySelectorAll('.toc-nav a');
if (!tocWrapper || !tocNav) return;
// =====================
// MOBILE TOGGLE
// =====================
if (tocToggle) {
tocToggle.addEventListener('click', function() {
tocWrapper.classList.toggle('collapsed');
});
}
// =====================
// SMOOTH SCROLLING
// =====================
tocLinks.forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const targetId = this.getAttribute('href');
const targetElement = document.querySelector(targetId);
if (targetElement) {
const yOffset = -80; // offset for sticky header
const y = targetElement.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
// Update URL without scrolling
history.pushState(null, null, targetId);
}
});
});
// =====================
// ACTIVE LINK HIGHLIGHT
// =====================
const headings = document.querySelectorAll('.markdown-body h1[id], .markdown-body h2[id], .markdown-body h3[id], .markdown-body h4[id], .markdown-body h5[id], .markdown-body h6[id]');
let activeHeading = null;
const observer = new IntersectionObserver(
(entries) => {
// Find all currently intersecting headings
const intersectingHeadings = [];
entries.forEach((entry) => {
if (entry.isIntersecting) {
intersectingHeadings.push({
element: entry.target,
top: entry.boundingClientRect.top
});
}
});
// If we have intersecting headings, find the one closest to the top
if (intersectingHeadings.length > 0) {
// Sort by distance from top (smallest positive value or largest negative)
intersectingHeadings.sort((a, b) => Math.abs(a.top) - Math.abs(b.top));
const closestHeading = intersectingHeadings[0].element;
const id = closestHeading.getAttribute('id');
if (id && id !== activeHeading) {
activeHeading = id;
const tocLink = document.querySelector(`.toc-nav a[href="#${id}"]`);
if (tocLink) {
// Remove active from all links
tocLinks.forEach(link => link.classList.remove('active'));
// Add active to current link
tocLink.classList.add('active');
}
}
}
},
{
rootMargin: '-80px 0px -80%',
threshold: [0, 0.25, 0.5, 0.75, 1]
}
);
// Observe all headings
headings.forEach((heading) => {
observer.observe(heading);
});
// Highlight first item by default
if (tocLinks.length > 0) {
tocLinks[0].classList.add('active');
activeHeading = tocLinks[0].getAttribute('href').substring(1);
}
})();
</script>
{{ end }}
</div>
</section>
{{ end }}