feat: enhance user experience with custom 404 page, breadcrumbs, and mobile ToC; add callout styles and language labels
This commit is contained in:
1 parent
deba0bf3f9
commit
72a45c1b7f
6 files changed
+420
-84
No files matched your search
+214
-63
@@ -1718,82 +1718,67 @@ footer.footer-modern a:hover {
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
COPY-TO-CLIPBOARD ON CODE BLOCKS
|
||||
CODE BLOCKS — header bar (language label + copy button)
|
||||
======================================== */
|
||||
.code-block {
|
||||
position: relative;
|
||||
margin: var(--space-md) 0;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* The frame is owned by .code-block, so the inner <pre> drops its own */
|
||||
.code-block pre {
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.code-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
padding: 0.3rem 0.5rem 0.3rem 0.9rem;
|
||||
background: var(--bg-subtle);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.code-lang {
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.code-copy {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
z-index: 2;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-subtle);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease, color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.code-block:hover .code-copy,
|
||||
.code-copy:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.code-copy:hover {
|
||||
color: var(--text-primary);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.code-copy.is-copied {
|
||||
opacity: 1;
|
||||
color: var(--accent-contrast);
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.code-copy svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* "Copied" toast — pops up just left of the button */
|
||||
.code-copy-toast {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: calc(0.5rem + 32px + 8px); /* clears the 32px button + gap */
|
||||
height: 32px;
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 0.6rem;
|
||||
gap: 0.35rem;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--accent);
|
||||
color: var(--accent-contrast);
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transform: translateX(6px);
|
||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||
font-family: var(--font-sans);
|
||||
cursor: pointer;
|
||||
transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.code-copy-toast.is-visible {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
.code-copy:hover { color: var(--text-primary); background: var(--bg-hover); }
|
||||
|
||||
.code-copy.is-copied {
|
||||
color: var(--accent-contrast);
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Touch devices have no hover — keep the button discoverable */
|
||||
@media (hover: none) {
|
||||
.code-copy { opacity: 0.7; }
|
||||
}
|
||||
.code-copy svg { display: block; }
|
||||
.code-copy-label { line-height: 1; }
|
||||
|
||||
/* ========================================
|
||||
GPU HINTS — promote animated layers ahead of time
|
||||
@@ -2086,6 +2071,172 @@ button:focus:not(:focus-visible) {
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
CALLOUTS / ADMONITIONS (> [!NOTE] etc.)
|
||||
======================================== */
|
||||
.callout {
|
||||
margin: var(--space-md) 0;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
border-left: 4px solid var(--cl-color, var(--accent));
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--cl-bg, var(--bg-subtle));
|
||||
color: var(--text-primary);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.callout > :last-child { margin-bottom: 0; }
|
||||
|
||||
.callout-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
margin-bottom: 0.4rem;
|
||||
font-weight: 700;
|
||||
color: var(--cl-color, var(--accent));
|
||||
}
|
||||
|
||||
.callout-icon { flex: 0 0 auto; }
|
||||
|
||||
.callout-note { --cl-color: #3b82f6; --cl-bg: rgba(59, 130, 246, 0.08); }
|
||||
.callout-tip { --cl-color: #22c55e; --cl-bg: rgba(34, 197, 94, 0.08); }
|
||||
.callout-important { --cl-color: #a855f7; --cl-bg: rgba(168, 85, 247, 0.08); }
|
||||
.callout-warning { --cl-color: #f59e0b; --cl-bg: rgba(245, 158, 11, 0.10); }
|
||||
.callout-caution { --cl-color: #ef4444; --cl-bg: rgba(239, 68, 68, 0.09); }
|
||||
|
||||
/* ========================================
|
||||
BREADCRUMBS
|
||||
======================================== */
|
||||
.breadcrumbs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
margin-bottom: var(--space-sm);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.breadcrumbs a { color: var(--text-secondary); }
|
||||
.breadcrumbs a:hover { color: var(--accent); text-decoration: none; }
|
||||
.breadcrumb-sep { color: var(--text-muted); }
|
||||
.breadcrumb-current {
|
||||
color: var(--text-muted);
|
||||
max-width: 50ch;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
CUSTOM 404
|
||||
======================================== */
|
||||
.page-404 .error-body {
|
||||
text-align: center;
|
||||
padding: var(--space-xl) var(--space-md);
|
||||
}
|
||||
|
||||
.error-code {
|
||||
margin: 0;
|
||||
font-size: 5rem;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.error-text { color: var(--text-secondary); margin-bottom: var(--space-lg); }
|
||||
.error-search { max-width: 420px; margin: 0 auto var(--space-lg); }
|
||||
.error-home { display: inline-flex; align-items: center; gap: 0.4rem; }
|
||||
|
||||
/* ========================================
|
||||
FIGURES (image + caption)
|
||||
======================================== */
|
||||
.content figure,
|
||||
.content-figure {
|
||||
margin: var(--space-md) 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.content figcaption,
|
||||
.content-figure figcaption {
|
||||
margin-top: 0.5rem;
|
||||
font-size: var(--font-size-sm);
|
||||
font-style: italic;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
MOBILE TABLE OF CONTENTS (floating button + drawer)
|
||||
======================================== */
|
||||
.toc-fab {
|
||||
position: fixed;
|
||||
left: 1.5rem;
|
||||
bottom: 1.5rem;
|
||||
z-index: 1035;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
color: var(--accent-contrast);
|
||||
box-shadow: var(--shadow-md);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toc-fab svg { display: block; }
|
||||
|
||||
.toc-drawer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.2s ease, visibility 0s linear 0.2s;
|
||||
}
|
||||
|
||||
.toc-drawer.is-open {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.toc-drawer-panel {
|
||||
width: 100%;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
padding: var(--space-lg);
|
||||
background: var(--bg-surface);
|
||||
border-top-left-radius: var(--radius-lg);
|
||||
border-top-right-radius: var(--radius-lg);
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
.toc-drawer.is-open .toc-drawer-panel { transform: translateY(0); }
|
||||
|
||||
.toc-drawer-title {
|
||||
margin-bottom: var(--space-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.toc-drawer .toc-list { list-style: none; padding: 0; margin: 0; }
|
||||
.toc-drawer .toc-link { display: block; padding: 0.5rem 0; color: var(--text-secondary); }
|
||||
.toc-drawer .toc-link:hover { color: var(--accent); text-decoration: none; }
|
||||
|
||||
/* The sidebar ToC covers desktop, so the floating button is mobile-only */
|
||||
@media (min-width: 768px) {
|
||||
.toc-fab,
|
||||
.toc-drawer { display: none !important; }
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
RESPONSIVE
|
||||
======================================== */
|
||||
|
||||
+159
-19
@@ -22,7 +22,8 @@
|
||||
goToSlide: I18N.goToSlide || 'Go to slide',
|
||||
prevImage: I18N.prevImage || 'Previous image',
|
||||
nextImage: I18N.nextImage || 'Next image',
|
||||
imageViewer: I18N.imageViewer || 'Image viewer'
|
||||
imageViewer: I18N.imageViewer || 'Image viewer',
|
||||
onThisPage: I18N.onThisPage || 'On this page'
|
||||
};
|
||||
|
||||
// Clipboard helper (Clipboard API with a legacy fallback).
|
||||
@@ -623,58 +624,82 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Inline SVGs — centred in their own viewBox, so no icon-font baseline /
|
||||
// side-bearing offsets to fight.
|
||||
var SVG_COPY = '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" ' +
|
||||
// Inline SVGs — centred in their own viewBox.
|
||||
var SVG_COPY = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" ' +
|
||||
'stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ' +
|
||||
'aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2"/>' +
|
||||
'<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
||||
var SVG_CHECK = '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" ' +
|
||||
var SVG_CHECK = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" ' +
|
||||
'stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" ' +
|
||||
'aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>';
|
||||
|
||||
// Pretty display names for common language classes.
|
||||
var LANG = {
|
||||
js: 'JavaScript', javascript: 'JavaScript', ts: 'TypeScript', typescript: 'TypeScript',
|
||||
json: 'JSON', html: 'HTML', xml: 'XML', css: 'CSS', scss: 'SCSS', bash: 'Bash',
|
||||
sh: 'Shell', shell: 'Shell', zsh: 'Zsh', console: 'Shell', powershell: 'PowerShell',
|
||||
ps1: 'PowerShell', python: 'Python', py: 'Python', php: 'PHP', go: 'Go', golang: 'Go',
|
||||
rust: 'Rust', rs: 'Rust', c: 'C', cpp: 'C++', csharp: 'C#', cs: 'C#', java: 'Java',
|
||||
kotlin: 'Kotlin', ruby: 'Ruby', rb: 'Ruby', sql: 'SQL', yaml: 'YAML', yml: 'YAML',
|
||||
toml: 'TOML', ini: 'INI', dockerfile: 'Dockerfile', docker: 'Dockerfile', nginx: 'Nginx',
|
||||
apache: 'Apache', diff: 'Diff', md: 'Markdown', markdown: 'Markdown', text: 'Text',
|
||||
plaintext: 'Text'
|
||||
};
|
||||
|
||||
function langName(code) {
|
||||
if (!code) return '';
|
||||
var m = (code.className || '').match(/language-([\w-]+)/i);
|
||||
if (!m) return '';
|
||||
var key = m[1].toLowerCase();
|
||||
if (key === 'tabs' || key === 'carousel') return ''; // theme fences, not languages
|
||||
return LANG[key] || (key.charAt(0).toUpperCase() + key.slice(1));
|
||||
}
|
||||
|
||||
function enhance(pre) {
|
||||
// Skip if already wrapped
|
||||
if (pre.parentNode && pre.parentNode.classList &&
|
||||
pre.parentNode.classList.contains('code-block')) return;
|
||||
|
||||
var code = pre.querySelector('code');
|
||||
|
||||
var wrap = document.createElement('div');
|
||||
wrap.className = 'code-block';
|
||||
pre.parentNode.insertBefore(wrap, pre);
|
||||
wrap.appendChild(pre);
|
||||
|
||||
// Header bar: language label (left) + copy button (right)
|
||||
var header = document.createElement('div');
|
||||
header.className = 'code-header';
|
||||
|
||||
var lang = document.createElement('span');
|
||||
lang.className = 'code-lang';
|
||||
lang.textContent = langName(code);
|
||||
header.appendChild(lang);
|
||||
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'code-copy';
|
||||
btn.setAttribute('aria-label', T.copyCode);
|
||||
btn.innerHTML = SVG_COPY;
|
||||
btn.innerHTML = SVG_COPY + '<span class="code-copy-label">' + T.copy + '</span>';
|
||||
header.appendChild(btn);
|
||||
|
||||
// "Copied" toast that pops up next to the button
|
||||
var toast = document.createElement('span');
|
||||
toast.className = 'code-copy-toast';
|
||||
toast.textContent = T.copied;
|
||||
wrap.insertBefore(header, pre);
|
||||
|
||||
var resetTimer = null;
|
||||
btn.addEventListener('click', function () {
|
||||
var code = pre.querySelector('code');
|
||||
var text = (code ? code.textContent : pre.textContent) || '';
|
||||
copyText(text).then(function () {
|
||||
btn.classList.add('is-copied');
|
||||
btn.innerHTML = SVG_CHECK;
|
||||
btn.innerHTML = SVG_CHECK + '<span class="code-copy-label">' + T.copied + '</span>';
|
||||
btn.setAttribute('aria-label', T.copied);
|
||||
toast.classList.add('is-visible');
|
||||
clearTimeout(resetTimer);
|
||||
resetTimer = setTimeout(function () {
|
||||
btn.classList.remove('is-copied');
|
||||
btn.innerHTML = SVG_COPY;
|
||||
btn.innerHTML = SVG_COPY + '<span class="code-copy-label">' + T.copy + '</span>';
|
||||
btn.setAttribute('aria-label', T.copyCode);
|
||||
toast.classList.remove('is-visible');
|
||||
}, 1000);
|
||||
}, 1200);
|
||||
}).catch(function () {});
|
||||
});
|
||||
|
||||
wrap.appendChild(btn);
|
||||
wrap.appendChild(toast);
|
||||
}
|
||||
|
||||
var pres = document.querySelectorAll('.content pre, .tab-panel pre');
|
||||
@@ -969,4 +994,119 @@
|
||||
});
|
||||
})();
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Callout / admonition boxes from GitHub-style blockquotes:
|
||||
> [!NOTE] / [!TIP] / [!IMPORTANT] / [!WARNING] / [!CAUTION] (or [!DANGER]).
|
||||
-------------------------------------------------------- */
|
||||
(function () {
|
||||
var content = document.querySelector('.content');
|
||||
if (!content) return;
|
||||
|
||||
function svg(inner) {
|
||||
return '<svg class="callout-icon" viewBox="0 0 24 24" width="18" height="18" ' +
|
||||
'fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" ' +
|
||||
'stroke-linejoin="round" aria-hidden="true">' + inner + '</svg>';
|
||||
}
|
||||
var ICONS = {
|
||||
note: svg('<circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/>'),
|
||||
tip: svg('<path d="M9 18h6M10 21h4"/><path d="M12 3a6 6 0 0 0-3.8 10.6c.5.5 1 1.4 1 2.4h5.6c0-1 .5-1.9 1-2.4A6 6 0 0 0 12 3z"/>'),
|
||||
important: svg('<circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/>'),
|
||||
warning: svg('<path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h16.9a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/><path d="M12 9v4M12 17h.01"/>'),
|
||||
caution: svg('<path d="M7.9 2h8.2L22 7.9v8.2L16.1 22H7.9L2 16.1V7.9z"/><path d="M12 8v4M12 16h.01"/>')
|
||||
};
|
||||
var LABELS = { note: 'Note', tip: 'Tip', important: 'Important', warning: 'Warning', caution: 'Caution' };
|
||||
var ALIAS = { danger: 'caution' };
|
||||
|
||||
Array.prototype.forEach.call(content.querySelectorAll('blockquote'), function (bq) {
|
||||
var first = bq.querySelector('p');
|
||||
if (!first) return;
|
||||
var m = first.innerHTML.match(/^\s*\[!(\w+)\]\s*(<br\s*\/?>)?\s*/i);
|
||||
if (!m) return;
|
||||
var type = m[1].toLowerCase();
|
||||
if (ALIAS[type]) type = ALIAS[type];
|
||||
if (!ICONS[type]) return;
|
||||
|
||||
first.innerHTML = first.innerHTML.replace(m[0], '');
|
||||
if (!first.innerHTML.trim()) { first.parentNode.removeChild(first); }
|
||||
|
||||
bq.classList.add('callout', 'callout-' + type);
|
||||
var title = document.createElement('div');
|
||||
title.className = 'callout-title';
|
||||
title.innerHTML = ICONS[type] + '<span>' + LABELS[type] + '</span>';
|
||||
bq.insertBefore(title, bq.firstChild);
|
||||
});
|
||||
})();
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Figures: turn a standalone content image WITH a title into a
|
||||
<figure> + <figcaption> (the markdown title becomes the caption).
|
||||
Runs after the lightbox so it can wrap the .zoomable-wrap.
|
||||
-------------------------------------------------------- */
|
||||
(function () {
|
||||
var content = document.querySelector('.content');
|
||||
if (!content) return;
|
||||
Array.prototype.forEach.call(content.querySelectorAll('img'), function (img) {
|
||||
var cap = img.getAttribute('title');
|
||||
if (!cap) return;
|
||||
if (img.closest('figure')) return;
|
||||
var node = img.closest('.zoomable-wrap') || img;
|
||||
var p = node.parentNode;
|
||||
// Only convert images that sit alone in their paragraph.
|
||||
if (!p || p.tagName !== 'P' || p.textContent.trim() !== '' || p.querySelectorAll('img').length !== 1) return;
|
||||
var fig = document.createElement('figure');
|
||||
fig.className = 'content-figure';
|
||||
fig.appendChild(node);
|
||||
var capEl = document.createElement('figcaption');
|
||||
capEl.textContent = cap;
|
||||
fig.appendChild(capEl);
|
||||
p.parentNode.replaceChild(fig, p);
|
||||
});
|
||||
})();
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Mobile ToC — a floating button that opens the table of contents in a
|
||||
drawer (the sidebar ToC is desktop-only). Mirrors the generated list.
|
||||
-------------------------------------------------------- */
|
||||
(function () {
|
||||
var tocNav = document.getElementById('toc-nav');
|
||||
if (!tocNav) return;
|
||||
var list = tocNav.querySelector('.toc-list');
|
||||
if (!list) return; // no headings -> no ToC
|
||||
|
||||
var fab = document.createElement('button');
|
||||
fab.type = 'button';
|
||||
fab.className = 'toc-fab';
|
||||
fab.setAttribute('aria-label', T.onThisPage);
|
||||
fab.innerHTML = '<svg viewBox="0 0 24 24" width="20" height="20" fill="none" ' +
|
||||
'stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ' +
|
||||
'aria-hidden="true"><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></svg>';
|
||||
|
||||
var drawer = document.createElement('div');
|
||||
drawer.className = 'toc-drawer';
|
||||
var panel = document.createElement('div');
|
||||
panel.className = 'toc-drawer-panel';
|
||||
var title = document.createElement('div');
|
||||
title.className = 'toc-drawer-title';
|
||||
title.textContent = T.onThisPage;
|
||||
panel.appendChild(title);
|
||||
var clone = list.cloneNode(true);
|
||||
panel.appendChild(clone);
|
||||
drawer.appendChild(panel);
|
||||
|
||||
document.body.appendChild(fab);
|
||||
document.body.appendChild(drawer);
|
||||
|
||||
function open() { drawer.classList.add('is-open'); document.body.style.overflow = 'hidden'; }
|
||||
function close() { drawer.classList.remove('is-open'); document.body.style.overflow = ''; }
|
||||
|
||||
fab.addEventListener('click', open);
|
||||
drawer.addEventListener('click', function (e) { if (e.target === drawer) close(); });
|
||||
Array.prototype.forEach.call(clone.querySelectorAll('a'), function (a) {
|
||||
a.addEventListener('click', close);
|
||||
});
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if ((e.key === 'Escape' || e.key === 'Esc') && drawer.classList.contains('is-open')) close();
|
||||
});
|
||||
})();
|
||||
|
||||
})();
|
||||
@@ -18,6 +18,11 @@
|
||||
"Copy link": "Copy link",
|
||||
"Link copied": "Link copied",
|
||||
"Related posts": "Related posts",
|
||||
"Home": "Home",
|
||||
"On this page": "On this page",
|
||||
"Page not found": "Page not found",
|
||||
"The page you are looking for does not exist or has moved.": "The page you are looking for does not exist or has moved.",
|
||||
"Back to home": "Back to home",
|
||||
"Previous": "Previous",
|
||||
"Next": "Next",
|
||||
"Updated": "Updated",
|
||||
|
||||
+2
-2
@@ -2,9 +2,9 @@
|
||||
"author": "spleenftw",
|
||||
"email": "",
|
||||
"website": "https://github.com/Spleenftw/blowdit",
|
||||
"version": "1.4.0",
|
||||
"version": "1.5.0",
|
||||
"releaseDate": "2026-06-02",
|
||||
"license": "MIT",
|
||||
"compatible": "3.22.0",
|
||||
"notes": "A Bludit theme inspired by the Blowfish Hugo theme. Five-theme swatch picker (Light/Dark/Nord/Dracula/Catppuccin), sticky ToC sidebar on articles, image lightbox with zoom, image carousel (```carousel fence), tabbed code blocks (```tabs fence), smart sidebar per page type, and clean responsive typography. v1.3.0: SEO (Open Graph, Twitter Card, JSON-LD, theme-color, cover preload), per-theme syntax highlighting (self-hosted highlight.js), copy-to-clipboard on code blocks, numbered pagination, inline-SVG icons (dropped the Bootstrap Icons webfont), removed jQuery/Bootstrap JS, deferred cached JS bundle, reading progress bar, back-to-top, randomised sidebar selection, lazy-loaded images, accessibility (skip link, ARIA, :focus-visible) and a print stylesheet. v1.4.0: canonical URL, WebSite + BreadcrumbList + author sameAs JSON-LD, og:image:alt / twitter:site+creator, cover-image aspect-ratio (CLS), heading anchor links, prev/next article nav, related posts, share buttons (X/LinkedIn/copy-link), mailto social icon, PWA web app manifest, 'updated on' date, and outbound-link hardening (noopener noreferrer)."
|
||||
"notes": "A Bludit theme inspired by the Blowfish Hugo theme. Five-theme swatch picker (Light/Dark/Nord/Dracula/Catppuccin), sticky ToC sidebar on articles, image lightbox with zoom, image carousel (```carousel fence), tabbed code blocks (```tabs fence), smart sidebar per page type, and clean responsive typography. v1.3.0: SEO (Open Graph, Twitter Card, JSON-LD, theme-color, cover preload), per-theme syntax highlighting (self-hosted highlight.js), copy-to-clipboard on code blocks, numbered pagination, inline-SVG icons (dropped the Bootstrap Icons webfont), removed jQuery/Bootstrap JS, deferred cached JS bundle, reading progress bar, back-to-top, randomised sidebar selection, lazy-loaded images, accessibility (skip link, ARIA, :focus-visible) and a print stylesheet. v1.4.0: canonical URL, WebSite + BreadcrumbList + author sameAs JSON-LD, og:image:alt / twitter:site+creator, cover-image aspect-ratio (CLS), heading anchor links, prev/next article nav, related posts, share buttons (X/LinkedIn/copy-link), mailto social icon, PWA web app manifest, 'updated on' date, and outbound-link hardening (noopener noreferrer). v1.5.0: callout/admonition boxes (> [!NOTE]/[!TIP]/[!WARNING]/...), code-block header bar with language label + copy button, visual breadcrumbs, custom 404 page, image figure/figcaption from the markdown title, a mobile table-of-contents drawer, and third-party resource hints."
|
||||
}
|
||||
@@ -21,6 +21,13 @@
|
||||
<link rel="preload" as="image" href="<?php echo htmlspecialchars($page->coverImage(true), ENT_QUOTES, 'UTF-8'); ?>" fetchpriority="high">
|
||||
<?php endif ?>
|
||||
|
||||
<?php if (!empty($blowditIsArticle)): ?>
|
||||
<!-- Warm up connections to article third-parties (comments + tweet embeds) -->
|
||||
<link rel="preconnect" href="https://utteranc.es" crossorigin>
|
||||
<link rel="dns-prefetch" href="https://platform.twitter.com">
|
||||
<link rel="dns-prefetch" href="https://cdn.syndication.twimg.com">
|
||||
<?php endif ?>
|
||||
|
||||
<!-- Set the colour theme as early as possible to avoid a white flash.
|
||||
Sets data-theme AND paints the html background/color-scheme inline, so the
|
||||
very first frame (before style.css loads) already uses the theme colour. -->
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
<?php if ($url->notFound()) : ?>
|
||||
|
||||
<!-- Custom 404 -->
|
||||
<article class="card card-modern my-5 page-404">
|
||||
<div class="card-body error-body">
|
||||
<p class="error-code">404</p>
|
||||
<h1 class="title"><?php echo $L->get('Page not found'); ?></h1>
|
||||
<p class="error-text"><?php echo $L->get('The page you are looking for does not exist or has moved.'); ?></p>
|
||||
<?php if (!empty($sidebarSearchHtml)) : ?>
|
||||
<div class="error-search"><?php echo $sidebarSearchHtml; ?></div>
|
||||
<?php endif ?>
|
||||
<a class="btn-primary-gradient error-home" href="<?php echo Theme::siteUrl(); ?>"><?php echo blowdit_icon('house'); ?> <?php echo $L->get('Back to home'); ?></a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<?php else : ?>
|
||||
|
||||
<!-- Post -->
|
||||
<article class="card card-modern my-5">
|
||||
|
||||
@@ -12,6 +29,20 @@
|
||||
<?php endif ?>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
<!-- Breadcrumbs (articles only) -->
|
||||
<?php if (!$page->isStatic() && !$url->notFound()) : ?>
|
||||
<nav class="breadcrumbs" aria-label="Breadcrumb">
|
||||
<a href="<?php echo Theme::siteUrl(); ?>"><?php echo $L->get('Home'); ?></a>
|
||||
<?php if ($page->categoryKey()) : ?>
|
||||
<span class="breadcrumb-sep" aria-hidden="true">›</span>
|
||||
<a href="<?php echo $page->categoryPermalink(); ?>"><?php echo blowdit_text($page->category()); ?></a>
|
||||
<?php endif ?>
|
||||
<span class="breadcrumb-sep" aria-hidden="true">›</span>
|
||||
<span class="breadcrumb-current" aria-current="page"><?php echo blowdit_text($page->title()); ?></span>
|
||||
</nav>
|
||||
<?php endif ?>
|
||||
|
||||
<!-- Title -->
|
||||
<h1 class="title"><?php echo blowdit_text($page->title()); ?></h1>
|
||||
|
||||
@@ -155,3 +186,5 @@
|
||||
<?php endif ?>
|
||||
|
||||
<?php } ?>
|
||||
|
||||
<?php endif ?>
|
||||
Reference in new issue
Block a user