Downloaded theme
This commit is contained in:
308
themes/minimal-black/layouts/404.html
Normal file
308
themes/minimal-black/layouts/404.html
Normal file
@@ -0,0 +1,308 @@
|
||||
{{ define "main" }}
|
||||
<section class="layout-page">
|
||||
<div class="error-page">
|
||||
<div class="error-content">
|
||||
<!-- Error Code -->
|
||||
<div class="error-code">404</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<h1 class="error-title">Page Not Found</h1>
|
||||
<p class="error-description">
|
||||
The page you're looking for doesn't exist or has been moved.
|
||||
</p>
|
||||
|
||||
<!-- Search Suggestion -->
|
||||
<div class="error-search">
|
||||
<p class="error-hint">
|
||||
Try searching for what you need, or return to the homepage.
|
||||
</p>
|
||||
<div class="error-actions">
|
||||
<button
|
||||
onclick="window.MinimalSearch.open()"
|
||||
class="btn-primary"
|
||||
aria-label="Search site"
|
||||
>
|
||||
<i class="fa-solid fa-magnifying-glass"></i>
|
||||
<span>Search Site</span>
|
||||
</button>
|
||||
<a href="/" class="btn-ghost">
|
||||
<i class="fa-solid fa-house"></i>
|
||||
<span>Go Home</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Helpful Links -->
|
||||
<div class="error-links">
|
||||
<h2 class="error-links-title">You might be interested in:</h2>
|
||||
<div class="error-links-grid">
|
||||
{{ with .Site.GetPage "/blog" }}
|
||||
<a href="{{ .Permalink }}" class="error-link-card">
|
||||
<i class="fa-regular fa-note-sticky"></i>
|
||||
<div>
|
||||
<strong>Blog</strong>
|
||||
<span>Read latest articles</span>
|
||||
</div>
|
||||
</a>
|
||||
{{ end }}
|
||||
|
||||
{{ with .Site.GetPage "/projects" }}
|
||||
<a href="{{ .Permalink }}" class="error-link-card">
|
||||
<i class="fa-regular fa-folder-open"></i>
|
||||
<div>
|
||||
<strong>Projects</strong>
|
||||
<span>Explore my work</span>
|
||||
</div>
|
||||
</a>
|
||||
{{ end }}
|
||||
|
||||
{{ with .Site.GetPage "/about" }}
|
||||
<a href="{{ .Permalink }}" class="error-link-card">
|
||||
<i class="fa-regular fa-user"></i>
|
||||
<div>
|
||||
<strong>About</strong>
|
||||
<span>Learn more about me</span>
|
||||
</div>
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alternative: Show Recent Posts -->
|
||||
{{ $recentPages := where .Site.RegularPages "Section" "in" (slice "blog" "projects") }}
|
||||
{{ $recentPages = first 3 $recentPages }}
|
||||
{{ if $recentPages }}
|
||||
<div class="error-recent">
|
||||
<h2 class="error-recent-title">Recent Content</h2>
|
||||
<ul class="error-recent-list">
|
||||
{{ range $recentPages }}
|
||||
<li>
|
||||
<a href="{{ .Permalink }}" class="error-recent-link">
|
||||
<span class="error-recent-icon">
|
||||
{{ if eq .Section "blog" }}
|
||||
<i class="fa-regular fa-note-sticky"></i>
|
||||
{{ else if eq .Section "projects" }}
|
||||
<i class="fa-regular fa-folder-open"></i>
|
||||
{{ else }}
|
||||
<i class="fa-regular fa-file"></i>
|
||||
{{ end }}
|
||||
</span>
|
||||
<span class="error-recent-text">
|
||||
<strong>{{ .Title }}</strong>
|
||||
{{ with .Description }}
|
||||
<small>{{ . }}</small>
|
||||
{{ end }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.error-page {
|
||||
min-height: 60vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem 0;
|
||||
}
|
||||
|
||||
.error-content {
|
||||
text-align: center;
|
||||
max-width: 42rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.error-code {
|
||||
font-size: 8rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
color: var(--color-accent);
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.error-description {
|
||||
font-size: 1.125rem;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.error-search {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.error-hint {
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.error-links {
|
||||
margin-top: 3rem;
|
||||
padding-top: 3rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.error-links-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.error-links-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.error-link-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1.25rem;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.75rem;
|
||||
text-decoration: none;
|
||||
color: var(--color-text);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.error-link-card:hover {
|
||||
border-color: var(--color-accent);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.error-link-card i {
|
||||
font-size: 1.5rem;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.error-link-card strong {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.error-link-card span {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.error-recent {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.error-recent-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 1rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.error-recent-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.error-recent-list li {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.error-recent-link {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.5rem;
|
||||
text-decoration: none;
|
||||
color: var(--color-text);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.error-recent-link:hover {
|
||||
border-color: var(--color-accent);
|
||||
background: color-mix(in srgb, var(--color-surface) 95%, var(--color-accent));
|
||||
}
|
||||
|
||||
.error-recent-icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--color-accent);
|
||||
font-size: 1.25rem;
|
||||
padding-top: 0.25rem;
|
||||
}
|
||||
|
||||
.error-recent-text {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.error-recent-text strong {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.error-recent-text small {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.error-code {
|
||||
font-size: 5rem;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.error-description {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.error-actions .btn-primary,
|
||||
.error-actions .btn-ghost {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.error-links-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{{ end }}
|
||||
@@ -0,0 +1,7 @@
|
||||
<div class="md-alert md-alert-note">
|
||||
<div class="md-mermaid">
|
||||
<pre class="mermaid">
|
||||
{{ .Text | safeHTML }}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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 -}}
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="md-mermaid">
|
||||
<pre class="mermaid">
|
||||
{{ .Inner | safeHTML }}
|
||||
</pre>
|
||||
</div>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,5 @@
|
||||
<h{{ .Level }} id="{{ .Anchor }}">
|
||||
<a href="#{{ .Anchor }}" class="md-heading-anchor">
|
||||
{{ .Text | safeHTML }}
|
||||
</a>
|
||||
</h{{ .Level }}>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,7 @@
|
||||
<a
|
||||
href="{{ .Destination | safeURL }}"
|
||||
{{ if strings.HasPrefix .Destination "http" }}target="_blank" rel="noopener"{{ end }}
|
||||
class="md-link"
|
||||
>
|
||||
{{ .Text | safeHTML }}
|
||||
</a>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,4 @@
|
||||
{{- $type := .Get "type" | default "note" -}}
|
||||
<div class="md-alert md-alert-{{ $type }}">
|
||||
{{ .Inner | markdownify }}
|
||||
</div>
|
||||
@@ -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>
|
||||
156
themes/minimal-black/layouts/_default/about-alternative.html
Normal file
156
themes/minimal-black/layouts/_default/about-alternative.html
Normal 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 }}
|
||||
77
themes/minimal-black/layouts/_default/about.html
Normal file
77
themes/minimal-black/layouts/_default/about.html
Normal 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 }}
|
||||
30
themes/minimal-black/layouts/_default/baseof.html
Normal file
30
themes/minimal-black/layouts/_default/baseof.html
Normal 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>
|
||||
30
themes/minimal-black/layouts/_default/list.html
Normal file
30
themes/minimal-black/layouts/_default/list.html
Normal 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>
|
||||
154
themes/minimal-black/layouts/_default/single.html
Normal file
154
themes/minimal-black/layouts/_default/single.html
Normal 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 }}
|
||||
48
themes/minimal-black/layouts/blog/list.html
Normal file
48
themes/minimal-black/layouts/blog/list.html
Normal file
@@ -0,0 +1,48 @@
|
||||
{{ define "main" }}
|
||||
<section class="layout-page">
|
||||
<div class="page-int section-stack">
|
||||
<header class="space-y-2">
|
||||
<h1 class="heading-page text-2xl sm:text-3xl">Blog</h1>
|
||||
{{ with .Site.Params.blogIntro }}
|
||||
<p class="max-w-xl text-sm text-muted">
|
||||
{{ . }}
|
||||
</p>
|
||||
{{ else }}
|
||||
<p class="max-w-xl text-sm text-muted">
|
||||
Notes on engineering, design, and building small, thoughtful tools.
|
||||
</p>
|
||||
{{ end }}
|
||||
</header>
|
||||
|
||||
{{/* Paginate all posts in this section */}}
|
||||
{{ $paginator := .Paginate .Pages }}
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
{{ range $paginator.Pages.ByDate.Reverse }}
|
||||
{{ partial "components/post-card.html" (dict "Page" . "Root" $) }}
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{/* Simple pagination controls */}}
|
||||
{{ if gt $paginator.TotalPages 1 }}
|
||||
<nav class="mt-6 flex items-center justify-between text-xs text-muted">
|
||||
{{ if $paginator.HasPrev }}
|
||||
<a href="{{ $paginator.Prev.URL }}" class="link-underline">
|
||||
← Newer posts
|
||||
</a>
|
||||
{{ else }}
|
||||
<span></span>
|
||||
{{ end }}
|
||||
|
||||
{{ if $paginator.HasNext }}
|
||||
<a href="{{ $paginator.Next.URL }}" class="link-underline">
|
||||
Older posts →
|
||||
</a>
|
||||
{{ else }}
|
||||
<span></span>
|
||||
{{ end }}
|
||||
</nav>
|
||||
{{ end }}
|
||||
</div>
|
||||
</section>
|
||||
{{ end }}
|
||||
12
themes/minimal-black/layouts/index.html
Normal file
12
themes/minimal-black/layouts/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
{{ define "main" }}
|
||||
<section class="layout-page">
|
||||
<div class="page-int section-stack section-stack--home">
|
||||
{{ $default := slice "hero" "now" "tech-marquee" "projects" "posts" }}
|
||||
{{ $sections := .Site.Params.home.sections | default $default }}
|
||||
|
||||
{{ range $sections }}
|
||||
{{ partial (printf "home/%s.html" .) $ }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</section>
|
||||
{{ end }}
|
||||
17
themes/minimal-black/layouts/index.json
Normal file
17
themes/minimal-black/layouts/index.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{{- $pages := slice -}}
|
||||
|
||||
{{- range .Site.RegularPages -}}
|
||||
{{- $summary := .Summary -}}
|
||||
{{- if not $summary -}}
|
||||
{{- $summary = .Description -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- $pages = $pages | append (dict
|
||||
"title" .Title
|
||||
"permalink" .Permalink
|
||||
"section" .Section
|
||||
"summary" (plainify $summary)
|
||||
) -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- dict "pages" $pages | jsonify -}}
|
||||
61
themes/minimal-black/layouts/index.webappmanifest
Normal file
61
themes/minimal-black/layouts/index.webappmanifest
Normal file
@@ -0,0 +1,61 @@
|
||||
{{- $title := .Site.Title -}}
|
||||
{{- $shortName := .Site.Params.brand | default .Site.Title -}}
|
||||
{{- $description := .Site.Params.description | default "A minimal, dark-mode first personal site" -}}
|
||||
{{- $themeColor := .Site.Params.manifest.themeColor | default "#a855f7" -}}
|
||||
{{- $bgColor := .Site.Params.manifest.backgroundColor | default "#000000" -}}
|
||||
{{- $lang := .Site.Language.Lang | default "en" -}}
|
||||
{
|
||||
"name": "{{ $title }}",
|
||||
"short_name": "{{ $shortName }}",
|
||||
"description": "{{ $description }}",
|
||||
"start_url": "{{ "/" | relURL }}",
|
||||
"scope": "{{ "/" | relURL }}",
|
||||
"display": "standalone",
|
||||
"background_color": "{{ $bgColor }}",
|
||||
"theme_color": "{{ $themeColor }}",
|
||||
"orientation": "portrait-primary",
|
||||
"icons": [
|
||||
{{- if .Site.Params.manifest.icons }}
|
||||
{{- range $i, $icon := .Site.Params.manifest.icons }}
|
||||
{{- if $i }},{{ end }}
|
||||
{
|
||||
"src": "{{ $icon.src | relURL }}",
|
||||
"sizes": "{{ $icon.sizes }}",
|
||||
"type": "{{ $icon.type | default "image/png" }}"
|
||||
{{- with $icon.purpose }},"purpose": "{{ . }}"{{ end }}
|
||||
}
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
{
|
||||
"src": "/icons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/apple-touch-icon.png",
|
||||
"sizes": "180x180",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/icons/favicon-32x32.png",
|
||||
"sizes": "32x32",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/icons/favicon-16x16.png",
|
||||
"sizes": "16x16",
|
||||
"type": "image/png"
|
||||
}
|
||||
{{- end }}
|
||||
],
|
||||
"categories": "{{ .Site.Params.manifest.categories | default (slice "blog" "portfolio" "developer") }}",
|
||||
"lang": "{{ $lang }}",
|
||||
"dir": "ltr"
|
||||
}
|
||||
46
themes/minimal-black/layouts/partials/analytics.html
Normal file
46
themes/minimal-black/layouts/partials/analytics.html
Normal file
@@ -0,0 +1,46 @@
|
||||
{{- if not hugo.IsServer -}}
|
||||
|
||||
<!-- Google Analytics (GA4) -->
|
||||
{{ with .Site.Params.analytics.googleAnalytics }}
|
||||
{{ if . }}
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id={{ . }}"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '{{ . }}');
|
||||
</script>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
<!-- Plausible Analytics -->
|
||||
{{ with .Site.Params.analytics.plausible }}
|
||||
{{ if .enabled }}
|
||||
<script defer data-domain="{{ .domain }}" src="{{ default "https://plausible.io/js/script.js" .scriptUrl }}"></script>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
<!-- Umami Analytics -->
|
||||
{{ with .Site.Params.analytics.umami }}
|
||||
{{ if .enabled }}
|
||||
<script defer src="{{ .scriptUrl }}" data-website-id="{{ .websiteId }}"></script>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
<!-- Fathom Analytics -->
|
||||
{{ with .Site.Params.analytics.fathom }}
|
||||
{{ if .enabled }}
|
||||
<script src="{{ .scriptUrl }}" data-site="{{ .siteId }}" defer></script>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
<!-- Custom Analytics Scripts -->
|
||||
{{ with .Site.Params.analytics.custom }}
|
||||
{{ if .head }}
|
||||
{{ range .head }}
|
||||
{{ . | safeHTML }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,53 @@
|
||||
{{/* props: Page (post page) */}}
|
||||
{{- $p := .Page -}}
|
||||
{{- $icon := $p.Params.icon | default "fa-regular fa-file-lines" -}}
|
||||
{{- $category := $p.Params.category | default "Article" -}}
|
||||
|
||||
<article class="card card-pad card-home card-home--post group">
|
||||
<a href="{{ $p.RelPermalink }}" class="card-home-body">
|
||||
<div class="card-home-header">
|
||||
<div class="card-home-icon card-home-icon--post">
|
||||
<i class="{{ $icon }}"></i>
|
||||
</div>
|
||||
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="min-w-0">
|
||||
<h3 class="truncate text-sm font-semibold tracking-tight group-hover:text-accent">
|
||||
{{ $p.Title }}
|
||||
</h3>
|
||||
|
||||
<div class="mt-0.5 flex flex-wrap items-center gap-2 text-[0.68rem] text-muted">
|
||||
{{ with $p.Date }}
|
||||
<span>{{ .Format "02 Jan 2006" }}</span>
|
||||
{{ end }}
|
||||
{{ with $p.ReadingTime }}
|
||||
<span>• {{ . }} min read</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ with $category }}
|
||||
<span class="card-badge card-badge--soft">
|
||||
{{ . }}
|
||||
</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ with $p.Params.description }}
|
||||
<p class="mt-2 text-xs text-muted">
|
||||
{{ . }}
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
{{ with $p.Params.tags }}
|
||||
<div class="mt-3 card-tag-row">
|
||||
{{ range first 3 . }}
|
||||
<span class="card-tag-pill">{{ . }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</a>
|
||||
</article>
|
||||
@@ -0,0 +1,87 @@
|
||||
{{/* props: Page (project page) */}}
|
||||
{{- $p := .Page -}}
|
||||
|
||||
{{- $icon := $p.Params.icon | default "fa-solid fa-folder-tree" -}}
|
||||
{{- $badge := cond ($p.Params.featured) "Featured" "" -}}
|
||||
|
||||
{{- $repo := $p.Params.repo -}}
|
||||
{{- $repoIcon := $p.Params.repoIcon | default "fa-brands fa-github" -}}
|
||||
{{- $repoLabel := $p.Params.repoLabel | default "Repo" -}}
|
||||
|
||||
{{- $demo := $p.Params.demo -}}
|
||||
{{- $demoIcon := $p.Params.demoIcon | default "fa-solid fa-play" -}}
|
||||
{{- $demoLabel := $p.Params.demoLabel | default "Demo" -}}
|
||||
|
||||
{{- $website := $p.Params.website -}}
|
||||
{{- $websiteIcon := $p.Params.websiteIcon | default "fa-solid fa-globe" -}}
|
||||
{{- $websiteLabel := $p.Params.websiteLabel | default "Website" -}}
|
||||
|
||||
<article class="card card-pad card-home card-home--project group">
|
||||
<!-- Entire main body is clickable -->
|
||||
<a href="{{ $p.RelPermalink }}" class="card-home-body">
|
||||
<div class="card-home-header">
|
||||
<div class="card-home-icon">
|
||||
<i class="{{ $icon }}"></i>
|
||||
</div>
|
||||
|
||||
<div class="min-w-0">
|
||||
<div class="inline-flex items-center gap-1">
|
||||
<h3 class="truncate text-sm font-semibold tracking-tight group-hover:text-accent">
|
||||
{{ $p.Title }}
|
||||
</h3>
|
||||
{{ with $badge }}
|
||||
<span class="card-badge">{{ . }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{ with $p.Params.subtitle }}
|
||||
<p class="mt-0.5 truncate text-[0.7rem] text-muted">
|
||||
{{ . }}
|
||||
</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ with $p.Params.description }}
|
||||
<p class="mt-2 text-xs text-muted">
|
||||
{{ . }}
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
{{ with $p.Params.stack }}
|
||||
<div class="mt-3 card-tag-row">
|
||||
{{ range . }}
|
||||
<span class="card-tag-pill">{{ . }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</a>
|
||||
|
||||
<!-- Footer buttons: repo, demo, website -->
|
||||
<div class="card-home-footer card-home-footer--buttons">
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
{{ with $repo }}
|
||||
<a href="{{ . }}" class="card-cta-btn" target="_blank" rel="noopener noreferrer">
|
||||
<i class="{{ $repoIcon }} text-[0.75rem]"></i>
|
||||
<span>{{ $repoLabel }}</span>
|
||||
</a>
|
||||
{{ end }}
|
||||
|
||||
{{ with $demo }}
|
||||
<a href="{{ . }}" class="card-cta-btn" target="_blank" rel="noopener noreferrer">
|
||||
<i class="{{ $demoIcon }} text-[0.75rem]"></i>
|
||||
<span>{{ $demoLabel }}</span>
|
||||
</a>
|
||||
{{ end }}
|
||||
|
||||
{{ with $website }}
|
||||
<a href="{{ . }}" class="card-cta-btn" target="_blank" rel="noopener noreferrer">
|
||||
<i class="{{ $websiteIcon }} text-[0.75rem]"></i>
|
||||
<span>{{ $websiteLabel }}</span>
|
||||
</a>
|
||||
{{ end }}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@@ -0,0 +1,12 @@
|
||||
{{/* props: Title, Url, Small (bool) */}} {{- $title := .Title | default
|
||||
"Section" -}} {{- $url := .Url -}} {{- $small := .Small | default false -}}
|
||||
|
||||
<div class="flex items-baseline justify-between gap-2">
|
||||
<h2 class="heading-section">{{ $title }}</h2>
|
||||
|
||||
{{ with $url }}
|
||||
<a href="{{ . | relURL }}" class="link-underline text-[0.72rem] text-muted">
|
||||
{{ if $small }}All{{ else }}View all{{ end }}
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
41
themes/minimal-black/layouts/partials/dark-toggle.html
Normal file
41
themes/minimal-black/layouts/partials/dark-toggle.html
Normal file
@@ -0,0 +1,41 @@
|
||||
<button
|
||||
type="button"
|
||||
class="theme-toggle"
|
||||
aria-label="Toggle dark mode"
|
||||
data-theme-toggle
|
||||
>
|
||||
<span data-theme-icon-light style="display: none">
|
||||
<!-- Sun icon -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path
|
||||
d="M12 3v2m0 14v2m9-9h-2M5 12H3m15.364-6.364-1.414 1.414M8.05 15.95l-1.414 1.414m0-11.314L8.05 8.05m9.9 9.9-1.414-1.414"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<span data-theme-icon-dark>
|
||||
<!-- Moon icon -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M21 12.79A9 9 0 0 1 12.21 3 7 7 0 1 0 21 12.79z" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
55
themes/minimal-black/layouts/partials/dock.html
Normal file
55
themes/minimal-black/layouts/partials/dock.html
Normal file
@@ -0,0 +1,55 @@
|
||||
{{- $isHome := .IsHome -}}
|
||||
|
||||
<div class="dock" data-dock>
|
||||
<div class="dock-inner">
|
||||
<!-- Actions panel -->
|
||||
<div class="dock-panel" data-dock-panel>
|
||||
{{ if not $isHome }}
|
||||
<!-- Back -->
|
||||
<button
|
||||
type="button"
|
||||
class="dock-action"
|
||||
data-dock-action="back"
|
||||
aria-label="Go back"
|
||||
>
|
||||
<i class="fa-solid fa-arrow-left text-[0.7rem]"></i>
|
||||
</button>
|
||||
<span class="dock-divider" aria-hidden="true"></span>
|
||||
{{ end }}
|
||||
|
||||
<!-- Search -->
|
||||
<button
|
||||
type="button"
|
||||
class="dock-action"
|
||||
data-dock-action="search"
|
||||
aria-label="Search"
|
||||
onclick="window.MinimalSearch && window.MinimalSearch.open()"
|
||||
>
|
||||
<i class="fa-solid fa-magnifying-glass text-[0.7rem]"></i>
|
||||
</button>
|
||||
|
||||
<!-- Divider -->
|
||||
<span class="dock-divider" aria-hidden="true"></span>
|
||||
|
||||
<!-- Back to top -->
|
||||
<button
|
||||
type="button"
|
||||
class="dock-action"
|
||||
data-dock-action="top"
|
||||
aria-label="Back to top"
|
||||
>
|
||||
<i class="fa-solid fa-arrow-up text-[0.7rem]"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Toggle -->
|
||||
<button
|
||||
type="button"
|
||||
class="dock-toggle"
|
||||
aria-label="Open quick actions"
|
||||
data-dock-toggle
|
||||
>
|
||||
<span class="dock-toggle-dots"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
56
themes/minimal-black/layouts/partials/footer.html
Normal file
56
themes/minimal-black/layouts/partials/footer.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<footer class="pb-6 pt-10">
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6">
|
||||
<div class="footer-shell">
|
||||
<div class="footer-inner">
|
||||
<!-- Left -->
|
||||
<div class="space-y-1">
|
||||
<p class="footer-small">
|
||||
© {{ now.Format "2006" }} {{ .Site.Title }} — All rights
|
||||
reserved.
|
||||
</p>
|
||||
|
||||
<p class="footer-small flex flex-wrap items-center gap-1">
|
||||
<span>Built with</span>
|
||||
|
||||
<a
|
||||
href="https://gohugo.io/"
|
||||
class="footer-link footer-float link-underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span>Hugo</span>
|
||||
</a>
|
||||
|
||||
<span>/</span>
|
||||
|
||||
<a
|
||||
href="{{ .Site.Params.themeGit }}"
|
||||
class="footer-link footer-float link-underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span>Minimal Black</span>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Right -->
|
||||
{{ with .Site.Params.social }}
|
||||
<div class="footer-links">
|
||||
{{ range . }}
|
||||
<a
|
||||
href="{{ .url }}"
|
||||
class="footer-link footer-float link-underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i class="{{ .icon }} text-[0.8rem]"></i>
|
||||
<span>{{ .label }}</span>
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
103
themes/minimal-black/layouts/partials/head.html
Normal file
103
themes/minimal-black/layouts/partials/head.html
Normal file
@@ -0,0 +1,103 @@
|
||||
{{- $title := cond (ne .Title "") (printf "%s | %s" .Title .Site.Title)
|
||||
.Site.Title -}}
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<title>{{ $title }}</title>
|
||||
|
||||
{{ partial "meta.html" . }}
|
||||
|
||||
<!-- Favicon -->
|
||||
{{ with .Site.Params.favicon }}
|
||||
<link rel="icon" type="image/x-icon" href="{{ . | relURL }}" />
|
||||
{{ else }}
|
||||
<!-- Default favicon paths -->
|
||||
{{ if fileExists "static/favicon.ico" }}
|
||||
<link rel="icon" type="image/x-icon" href="{{ "favicon.ico" | relURL }}" />
|
||||
{{ end }}
|
||||
{{ if fileExists "static/favicon.png" }}
|
||||
<link rel="icon" type="image/png" href="{{ "favicon.png" | relURL }}" />
|
||||
{{ end }}
|
||||
{{ if fileExists "static/favicon.svg" }}
|
||||
<link rel="icon" type="image/svg+xml" href="{{ "favicon.svg" | relURL }}" />
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
<!-- Apple Touch Icon -->
|
||||
{{ if .Site.Params.appleTouchIcon }}
|
||||
<link rel="apple-touch-icon" href="{{ . | relURL }}" />
|
||||
{{ else if fileExists "static/apple-touch-icon.png" }}
|
||||
<link rel="apple-touch-icon" href="{{ "apple-touch-icon.png" | relURL }}" />
|
||||
{{ end }}
|
||||
|
||||
<!-- Web App Manifest -->
|
||||
{{ with .Site.GetPage "/" }}
|
||||
{{ range .OutputFormats }}
|
||||
{{ if eq .Name "webappmanifest" }}
|
||||
<link rel="manifest" href="{{ .Permalink }}" />
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
<link rel="stylesheet" href="{{ "css/main.css" | relURL }}">
|
||||
|
||||
{{ if .Site.Params.icons.useFontAwesome }}
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css"
|
||||
>
|
||||
{{ end }}
|
||||
|
||||
{{ if .Site.Params.icons.useDevicon }}
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/gh/devicons/devicon@v2.17.0/devicon.min.css"
|
||||
>
|
||||
{{ end }}
|
||||
|
||||
<!-- GLightbox -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/glightbox/dist/css/glightbox.min.css"
|
||||
/>
|
||||
|
||||
<!-- Justified Gallery (Vanilla) -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/justified-gallery@3.8.2/dist/css/justifiedGallery.min.css"
|
||||
/>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
||||
<script>
|
||||
mermaid.initialize({ theme: "dark" });
|
||||
</script>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var stored = localStorage.getItem("theme");
|
||||
var systemDark =
|
||||
window.matchMedia &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
var defaultTheme =
|
||||
'{{ default "system" .Site.Params.theme.defaultTheme }}';
|
||||
|
||||
var theme =
|
||||
stored ||
|
||||
(defaultTheme === "dark"
|
||||
? "dark"
|
||||
: defaultTheme === "light"
|
||||
? "light"
|
||||
: systemDark
|
||||
? "dark"
|
||||
: "light");
|
||||
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
} catch (e) {
|
||||
document.documentElement.setAttribute("data-theme", "light");
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Analytics -->
|
||||
{{ partial "analytics.html" . }}
|
||||
86
themes/minimal-black/layouts/partials/header.html
Normal file
86
themes/minimal-black/layouts/partials/header.html
Normal file
@@ -0,0 +1,86 @@
|
||||
<header class="pt-6">
|
||||
{{/* Brand string for logo/monogram */}}
|
||||
{{- $brand := .Site.Params.brand | default .Site.Title}}
|
||||
{{- $mono := upper (substr $brand 0 2) -}}
|
||||
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6">
|
||||
<!-- Floating nav container -->
|
||||
<div class="nav-shell">
|
||||
<div class="nav-inner">
|
||||
<!-- Brand: logo or monogram, always links to root -->
|
||||
<a href="{{ "/" | relURL }}" class="flex items-center gap-2">
|
||||
{{ with .Site.Params.logo }}
|
||||
<div class="flex h-8 w-8 items-center justify-center overflow-hidden rounded-full bg-surface">
|
||||
<img
|
||||
src="{{ . | relURL }}"
|
||||
alt="{{ $brand }}"
|
||||
class="h-full w-full object-cover"
|
||||
>
|
||||
</div>
|
||||
{{ else }}
|
||||
<div class="logo-badge">
|
||||
{{ $mono }}
|
||||
</div>
|
||||
{{ end }}
|
||||
<span class="text-xs font-semibold tracking-wide">
|
||||
{{ $brand }}
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Desktop nav (configurable) -->
|
||||
<nav class="hidden items-center gap-5 md:flex">
|
||||
{{- $current := . -}}
|
||||
{{- range .Site.Menus.main }}
|
||||
<a
|
||||
href="{{ .URL | relURL }}"
|
||||
class="nav-link link-underline flex items-center gap-1 {{ if $current.IsMenuCurrent "main" . }}text-text{{ end }}"
|
||||
>
|
||||
{{ with .Params.icon }}
|
||||
<i class="{{ . }} text-[0.75rem]"></i>
|
||||
{{ end }}
|
||||
<span>{{ .Name }}</span>
|
||||
</a>
|
||||
{{- end }}
|
||||
</nav>
|
||||
|
||||
<!-- Theme toggle, sized to match nav -->
|
||||
{{ partial "dark-toggle.html" . }}
|
||||
|
||||
<!-- Mobile menu button -->
|
||||
<button
|
||||
type="button"
|
||||
class="ml-1 inline-flex items-center justify-center rounded-full border border-border bg-bg p-2 text-muted shadow-sm hover:text-accent md:hidden"
|
||||
aria-label="Toggle navigation"
|
||||
data-mobile-nav-toggle
|
||||
>
|
||||
<span class="sr-only">Open navigation</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="4" y1="6" x2="20" y2="6" />
|
||||
<line x1="4" y1="12" x2="20" y2="12" />
|
||||
<line x1="4" y1="18" x2="20" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile nav (uses same menu config) -->
|
||||
<div class="mx-auto mt-2 max-w-7xl px-4 sm:px-6 md:hidden">
|
||||
<nav
|
||||
class="hidden rounded-2xl border border-border bg-surface px-4 py-3 text-sm text-muted shadow-md"
|
||||
data-mobile-nav
|
||||
>
|
||||
{{ range .Site.Menus.main }}
|
||||
<a href="{{ .URL | relURL }}" class="flex items-center gap-2 py-1.5">
|
||||
{{ with .Params.icon }}
|
||||
<i class="{{ . }} text-[0.8rem]"></i>
|
||||
{{ end }}
|
||||
<span>{{ .Name }}</span>
|
||||
</a>
|
||||
{{ end }}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
94
themes/minimal-black/layouts/partials/home/hero.html
Normal file
94
themes/minimal-black/layouts/partials/home/hero.html
Normal file
@@ -0,0 +1,94 @@
|
||||
{{- $hero := .Site.Params.hero -}}
|
||||
|
||||
<div class="grid gap-8 md:grid-cols-[minmax(0,2fr)_minmax(0,1.2fr)] items-start">
|
||||
<div class="space-y-5 animate-fade-up">
|
||||
<!-- Badge + availability -->
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
{{ with $hero.badge }}
|
||||
<p class="eyebrow font-medium text-sm text-accent">{{ . }}</p>
|
||||
{{ end }}
|
||||
|
||||
{{ if $hero.available }}
|
||||
<span class="badge-available px-3">
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-white/95"></span>
|
||||
<span>{{ default "Available for work" $hero.availableLabel }}</span>
|
||||
</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<h1 class="heading-page text-3xl sm:text-4xl">
|
||||
{{ default "Hi, I’m Your Name." $hero.title }}
|
||||
</h1>
|
||||
{{ with $hero.role }}
|
||||
<p class="text-sm font-medium text-muted">
|
||||
{{ . }}
|
||||
</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{ with $hero.summary }}
|
||||
<p class="max-w-xl text-sm text-muted">
|
||||
{{ . }}
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
<!-- Meta row: location + focus -->
|
||||
<div class="mt-3 flex flex-wrap gap-2 text-[0.7rem] text-muted">
|
||||
{{ with $hero.location }}
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full border border-border bg-surface/90 px-3 py-1.5">
|
||||
<i class="fa-solid fa-location-dot text-[0.8rem]"></i>
|
||||
<span class="font-medium">{{ . }}</span>
|
||||
</span>
|
||||
{{ end }}
|
||||
|
||||
{{ with $hero.focus }}
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full border border-border bg-surface/80 px-3 py-1.5">
|
||||
<i class="fa-regular fa-circle-dot text-[0.8rem]"></i>
|
||||
<span>{{ . }}</span>
|
||||
</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<!-- Optional highlight pills -->
|
||||
{{ with $hero.highlights }}
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
{{ range . }}
|
||||
<span class="inline-flex items-center rounded-full border border-border bg-surface/80 px-3 py-1 text-[0.7rem] text-muted">
|
||||
{{ .label }}
|
||||
</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<!-- CTAs -->
|
||||
<div class="mt-4 flex flex-wrap items-center gap-3">
|
||||
{{ with $hero.primary }}
|
||||
<a href="{{ .href | default "/projects/" }}" class="btn-primary">
|
||||
{{ .label | default "View projects" }}
|
||||
</a>
|
||||
{{ end }}
|
||||
|
||||
{{ with $hero.secondary }}
|
||||
<a href="{{ .href | default "/blog/" }}" class="btn-ghost link-underline">
|
||||
{{ .label | default "Read the blog" }}
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Avatar ONLY if provided -->
|
||||
{{ with $hero.avatar }}
|
||||
<div class="order-first md:order-none">
|
||||
<div class="card card-pad flex items-center justify-center md:justify-end">
|
||||
<div class="h-24 w-24 sm:h-28 sm:w-28 overflow-hidden rounded-2xl border border-border bg-surface shadow-md">
|
||||
<img
|
||||
src="{{ . | relURL }}"
|
||||
alt="{{ $hero.title | default $.Site.Title }}"
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
28
themes/minimal-black/layouts/partials/home/now.html
Normal file
28
themes/minimal-black/layouts/partials/home/now.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{{- $hero := .Site.Params.hero -}}
|
||||
{{- $home := .Site.Params.home -}}
|
||||
|
||||
{{ if $home.showNowSection }}
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<h2 class="heading-section">{{ default "Now" $hero.nowLabel }}</h2>
|
||||
<div class="card card-pad text-xs text-muted">
|
||||
{{ with $hero.nowIntro }}
|
||||
<p class="mb-2">{{ . }}</p>
|
||||
{{ end }}
|
||||
|
||||
{{ with $hero.now }}
|
||||
<ul class="list-disc space-y-1 pl-4">
|
||||
{{ range . }}
|
||||
<li>{{ . }}</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ else }}
|
||||
<p>
|
||||
Add a <code>hero.now</code> list in your config to describe what you’re
|
||||
focused on right now.
|
||||
</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
38
themes/minimal-black/layouts/partials/home/posts.html
Normal file
38
themes/minimal-black/layouts/partials/home/posts.html
Normal file
@@ -0,0 +1,38 @@
|
||||
{{- $home := .Site.Params.home -}}
|
||||
{{ if ne $home.showLatestPosts false }}
|
||||
<div class="space-y-3 animate-fade-up">
|
||||
<div class="space-y-1">
|
||||
<h2 class="heading-section">
|
||||
{{ default "Latest writing" $home.blogTitle }}
|
||||
</h2>
|
||||
{{ with $home.blogSubtitle }}
|
||||
<p class="text-xs text-muted">
|
||||
{{ . }}
|
||||
</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{ $limit := cond (gt (int $home.latestPostsLimit) 0) (int $home.latestPostsLimit) 3 }}
|
||||
{{ $posts := first $limit (where .Site.RegularPages "Section" "blog") }}
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
{{ range $posts }}
|
||||
{{ partial "components/post-card.html" (dict "Page" . "Root" $) }}
|
||||
{{ else }}
|
||||
<p class="text-xs text-muted">
|
||||
No posts yet. Add some under <code>content/blog</code>.
|
||||
</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex justify-end">
|
||||
<a
|
||||
href="{{ "/blog/" | relURL }}"
|
||||
class="btn-primary btn-primary-sm inline-flex items-center gap-2"
|
||||
>
|
||||
<span>View all posts</span>
|
||||
<i class="fa-solid fa-arrow-right-long text-[0.8rem]"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
43
themes/minimal-black/layouts/partials/home/projects.html
Normal file
43
themes/minimal-black/layouts/partials/home/projects.html
Normal file
@@ -0,0 +1,43 @@
|
||||
{{- $home := .Site.Params.home -}}
|
||||
{{ if ne $home.showFeaturedProjects false }}
|
||||
<div class="space-y-3 animate-fade-up">
|
||||
<div class="space-y-1">
|
||||
<h2 class="heading-section">
|
||||
{{ default "Selected work" $home.projectsTitle }}
|
||||
</h2>
|
||||
{{ with $home.projectsSubtitle }}
|
||||
<p class="text-xs text-muted">
|
||||
{{ . }}
|
||||
</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{ $limit := cond (gt (int $home.featuredProjectsLimit) 0) (int $home.featuredProjectsLimit) 3 }}
|
||||
{{ $allProjects := where .Site.RegularPages "Section" "projects" }}
|
||||
{{ $featured := where $allProjects "Params.featured" true }}
|
||||
{{ if not (gt (len $featured) 0) }}
|
||||
{{ $featured = $allProjects }}
|
||||
{{ end }}
|
||||
{{ $list := first $limit $featured }}
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
{{ range $list }}
|
||||
{{ partial "components/project-card.html" (dict "Page" . "Root" $) }}
|
||||
{{ else }}
|
||||
<p class="text-xs text-muted">
|
||||
No projects yet. Add some under <code>content/projects</code>.
|
||||
</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex justify-end">
|
||||
<a
|
||||
href="{{ "/projects/" | relURL }}"
|
||||
class="btn-primary btn-primary-sm inline-flex items-center gap-2"
|
||||
>
|
||||
<span>View all projects</span>
|
||||
<i class="fa-solid fa-arrow-right-long text-[0.8rem]"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
59
themes/minimal-black/layouts/partials/home/tech-marquee.html
Normal file
59
themes/minimal-black/layouts/partials/home/tech-marquee.html
Normal file
@@ -0,0 +1,59 @@
|
||||
{{- $home := .Site.Params.home -}}
|
||||
{{- $hero := .Site.Params.hero -}}
|
||||
{{- $techMain := $home.tech -}}
|
||||
{{- $techReverse := $home.techReverse -}}
|
||||
{{- $variant := $home.techVariant | default "wide" -}}
|
||||
|
||||
{{ with $techMain }}
|
||||
<div class="space-y-5 animate-fade-up">
|
||||
<div class="space-y-2">
|
||||
<h3 class="heading-section text-base">
|
||||
{{ default "What I Work With" $hero.techMarqueeLabel }}
|
||||
</h3>
|
||||
|
||||
<div class="tech-strip {{ if eq $variant "compact" }}tech-strip--compact{{ else }}tech-strip--wide{{ end }}">
|
||||
<!-- primary row (always rendered) -->
|
||||
<div class="tech-strip-track tech-strip-track--primary">
|
||||
{{ range . }}
|
||||
<div class="tech-strip-item">
|
||||
{{ with .icon }}
|
||||
<i class="{{ . }} tech-icon"></i>
|
||||
{{ end }}
|
||||
<span class="font-medium">{{ .label }}</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ range . }}
|
||||
<div class="tech-strip-item" aria-hidden="true">
|
||||
{{ with .icon }}
|
||||
<i class="{{ . }} tech-icon"></i>
|
||||
{{ end }}
|
||||
<span class="font-medium">{{ .label }}</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<!-- secondary row, reverse (only if techReverse is defined) -->
|
||||
{{ with $techReverse }}
|
||||
<div class="tech-strip-track tech-strip-track--secondary">
|
||||
{{ range . }}
|
||||
<div class="tech-strip-item">
|
||||
{{ with .icon }}
|
||||
<i class="{{ . }} tech-icon"></i>
|
||||
{{ end }}
|
||||
<span class="font-medium">{{ .label }}</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ range . }}
|
||||
<div class="tech-strip-item" aria-hidden="true">
|
||||
{{ with .icon }}
|
||||
<i class="{{ . }} tech-icon"></i>
|
||||
{{ end }}
|
||||
<span class="font-medium">{{ .label }}</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
7
themes/minimal-black/layouts/partials/meta.html
Normal file
7
themes/minimal-black/layouts/partials/meta.html
Normal file
@@ -0,0 +1,7 @@
|
||||
{{- with .Description -}}
|
||||
<meta name="description" content="{{ . }}" />
|
||||
{{- else -}}
|
||||
<meta name="description" content="{{ .Site.Params.description }}" />
|
||||
{{- end }} {{- with .Site.Params.author }}
|
||||
<meta name="author" content="{{ . }}" />
|
||||
{{- end }}
|
||||
59
themes/minimal-black/layouts/partials/search-overlay.html
Normal file
59
themes/minimal-black/layouts/partials/search-overlay.html
Normal file
@@ -0,0 +1,59 @@
|
||||
<div class="search-overlay" data-search-overlay>
|
||||
<div class="search-overlay-backdrop" data-search-close></div>
|
||||
|
||||
<div class="search-panel">
|
||||
<div class="search-panel-header">
|
||||
<div class="search-input-wrap">
|
||||
<i class="fa-solid fa-magnifying-glass text-[0.8rem] text-muted"></i>
|
||||
<input
|
||||
type="search"
|
||||
class="search-input"
|
||||
placeholder="Search posts..."
|
||||
autocomplete="off"
|
||||
data-search-input
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="search-close"
|
||||
data-search-close
|
||||
aria-label="Close search"
|
||||
>
|
||||
<i class="fa-solid fa-xmark text-[0.8rem]"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="search-panel-body">
|
||||
<div class="search-results" data-search-results>
|
||||
<div class="search-empty-state">
|
||||
<div class="search-empty-icon">
|
||||
<i class="fa-solid fa-magnifying-glass text-[1rem]"></i>
|
||||
</div>
|
||||
<p class="search-empty-title">Start searching</p>
|
||||
<p class="search-empty-subtitle">
|
||||
Enter keywords to search articles.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-footer-hints">
|
||||
<div class="search-hint-key">
|
||||
<span>↑↓</span>
|
||||
<span>Navigate</span>
|
||||
</div>
|
||||
<div class="search-hint-key">
|
||||
<span>↵</span>
|
||||
<span>Select</span>
|
||||
</div>
|
||||
<div class="search-hint-key">
|
||||
<span>ESC</span>
|
||||
<span>Close</span>
|
||||
</div>
|
||||
<div class="search-hint-key search-hint-right">
|
||||
<span>Ctrl</span><span>K</span>
|
||||
<span>Shortcut</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
50
themes/minimal-black/layouts/projects/list.html
Normal file
50
themes/minimal-black/layouts/projects/list.html
Normal file
@@ -0,0 +1,50 @@
|
||||
{{ define "main" }}
|
||||
<section class="layout-page">
|
||||
<div class="page-int section-stack">
|
||||
<header class="space-y-2">
|
||||
<h1 class="heading-page text-2xl sm:text-3xl">Projects</h1>
|
||||
|
||||
{{ with .Site.Params.projectsIntro }}
|
||||
<p class="max-w-xl text-sm text-muted">
|
||||
{{ . }}
|
||||
</p>
|
||||
{{ else }}
|
||||
<p class="max-w-xl text-sm text-muted">
|
||||
A selection of things I’ve been building – themes, tools, and experiments.
|
||||
</p>
|
||||
{{ end }}
|
||||
</header>
|
||||
|
||||
<!-- Cards -->
|
||||
{{/* you can tweak sorting if you want; this is newest first */}}
|
||||
{{ $paginator := .Paginate (.Pages.ByDate.Reverse) }}
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
{{ range $paginator.Pages }}
|
||||
{{ partial "components/project-card.html" (dict "Page" . "Root" $) }}
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{{ if gt $paginator.TotalPages 1 }}
|
||||
<nav class="mt-6 flex items-center justify-between text-xs text-muted">
|
||||
{{ if $paginator.HasPrev }}
|
||||
<a href="{{ $paginator.Prev.URL }}" class="link-underline">
|
||||
← Newer projects
|
||||
</a>
|
||||
{{ else }}
|
||||
<span></span>
|
||||
{{ end }}
|
||||
|
||||
{{ if $paginator.HasNext }}
|
||||
<a href="{{ $paginator.Next.URL }}" class="link-underline">
|
||||
Older projects →
|
||||
</a>
|
||||
{{ else }}
|
||||
<span></span>
|
||||
{{ end }}
|
||||
</nav>
|
||||
{{ end }}
|
||||
</div>
|
||||
</section>
|
||||
{{ end }}
|
||||
Reference in New Issue
Block a user