testing dark/light mode

This commit is contained in:
2026-08-13 12:21:37 -05:00
parent 1aad2224e2
commit d7d60dc0f6
5 changed files with 277 additions and 316 deletions

371
post.html
View File

@@ -1,279 +1,144 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Post</title>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Post</title>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js/styles/github.min.css">
<script src="https://cdn.jsdelivr.net/npm/highlight.js/lib/common.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js/styles/github.min.css">
<script src="https://cdn.jsdelivr.net/npm/highlight.js/lib/common.min.js"></script>
<style>
:root {
--ink: #26241f;
--ink-soft: #6b6a63;
--paper: #fbfaf7;
--rule: #e4e1d8;
--accent: #3a5a40;
--code-bg: #f1efe8;
}
<script>
(function() {
var stored = localStorage.getItem('theme');
if (stored === 'light') document.documentElement.setAttribute('data-theme', 'light');
})();
* { box-sizing: border-box; }
function toggleTheme() {
var isLight = document.documentElement.getAttribute('data-theme') === 'light';
if (isLight) {
document.documentElement.removeAttribute('data-theme');
localStorage.setItem('theme', 'dark');
} else {
document.documentElement.setAttribute('data-theme', 'light');
localStorage.setItem('theme', 'light');
}
}
</script>
html { -webkit-text-size-adjust: 100%; }
<link rel="stylesheet" href="styles/main.css">
body {
margin: 0;
background: var(--paper);
color: var(--ink);
font-family: Georgia, 'Iowan Old Style', 'Palatino Linotype', serif;
line-height: 1.7;
}
</head>
<body>
.wrap {
max-width: 680px;
margin: 0 auto;
padding: 4rem 1.5rem 6rem;
}
<div class="wrap">
<button id="theme-toggle" onclick="toggleTheme()">toggle theme</button>
<a class="back-link" href="index.html">&larr; Return home</a>
<p class="meta" id="meta" hidden></p>
<div id="content"><p class="state">Loading post&hellip;</p></div>
</div>
.back-link {
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
font-size: 0.85rem;
letter-spacing: 0.02em;
text-transform: uppercase;
color: var(--ink-soft);
text-decoration: none;
margin-bottom: 3rem;
border-bottom: 1px solid transparent;
transition: border-color 0.15s ease, color 0.15s ease;
}
.back-link:hover {
color: var(--accent);
border-color: var(--accent);
}
<script>
function parseFrontMatter(raw) {
const match = raw.match(/^---\r?\n([\s\S]+?)\r?\n---\r?\n([\s\S]*)$/);
if (!match) return { meta: {}, body: raw };
.meta {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
font-size: 0.85rem;
color: var(--ink-soft);
margin: 0 0 0.5rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
const meta = {};
match[1].split(/\r?\n/).forEach(line => {
const idx = line.indexOf(':');
if (idx === -1) return;
const key = line.slice(0, idx).trim();
let value = line.slice(idx + 1).trim();
// strip optional surrounding quotes: title: "My Post"
value = value.replace(/^["'](.*)["']$/, '$1');
meta[key] = value;
});
#content h1:first-child {
font-size: 2.1rem;
line-height: 1.25;
margin: 0 0 0.75rem;
letter-spacing: -0.01em;
}
return { meta, body: match[2] };
}
#content h1, #content h2, #content h3 {
font-family: Georgia, serif;
color: var(--ink);
margin-top: 2.2em;
margin-bottom: 0.6em;
}
function formatDate(dateStr) {
if (!dateStr) return '';
const d = new Date(dateStr);
if (isNaN(d)) return dateStr; // fall back to raw string if unparseable
return d.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' });
}
#content h2 { font-size: 1.5rem; border-bottom: 1px solid var(--rule); padding-bottom: 0.3em; }
#content h3 { font-size: 1.2rem; }
function showError(message) {
document.getElementById('content').innerHTML =
`<p class="state error">${message}</p>`;
}
#content p { margin: 1.1em 0; }
// ---- main ------------------------------------------------------------
#content a { color: var(--accent); text-decoration-color: rgba(58,90,64,0.35); }
#content a:hover { text-decoration-color: var(--accent); }
marked.setOptions({
highlight: (code, lang) => {
try {
return lang
? hljs.highlight(code, { language: lang }).value
: hljs.highlightAuto(code).value;
} catch (e) {
return code;
}
}
});
#content img {
max-width: 100%;
height: auto;
border-radius: 4px;
margin: 1.5em 0;
}
async function loadPost() {
const params = new URLSearchParams(window.location.search);
const file = params.get('file');
#content blockquote {
margin: 1.5em 0;
padding: 0.2em 1.2em;
border-left: 3px solid var(--accent);
color: var(--ink-soft);
font-style: italic;
}
if (!file) {
showError('No post specified. Link to this page as <code>post.html?file=your-post.md</code>.');
return;
}
#content ul, #content ol {
padding-left: 1.4em;
}
#content li { margin: 0.4em 0; }
// Guard against path traversal — keep the fetch confined to posts/
if (file.includes('..') || file.startsWith('/')) {
showError('Invalid post path.');
return;
}
#content hr {
border: none;
border-top: 1px solid var(--rule);
margin: 2.5em 0;
}
try {
const res = await fetch('posts/' + file);
if (!res.ok) throw new Error('HTTP ' + res.status);
const raw = await res.text();
#content code {
font-family: 'SF Mono', Menlo, Consolas, monospace;
font-size: 0.85em;
background: var(--code-bg);
padding: 0.15em 0.4em;
border-radius: 3px;
}
const { meta, body } = parseFrontMatter(raw);
const html = marked.parse(body);
#content pre {
background: var(--code-bg);
padding: 1em 1.2em;
border-radius: 6px;
overflow-x: auto;
line-height: 1.5;
}
#content pre code {
background: none;
padding: 0;
font-size: 0.85em;
}
document.getElementById('content').innerHTML = html;
#content table {
width: 100%;
border-collapse: collapse;
margin: 1.5em 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
font-size: 0.9rem;
}
#content th, #content td {
text-align: left;
padding: 0.5em 0.8em;
border-bottom: 1px solid var(--rule);
}
#content th { color: var(--ink-soft); font-weight: 600; }
// Title: front matter > first H1 in the doc > filename
let title = meta.title;
if (!title) {
const h1 = document.querySelector('#content h1');
title = h1 ? h1.textContent : file.replace(/\.md$/, '');
}
document.title = title;
.state {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
color: var(--ink-soft);
font-size: 0.95rem;
}
// Inject an H1 if front matter supplied a title but the body didn't have one
if (meta.title && !document.querySelector('#content h1:first-child')) {
const h1 = document.createElement('h1');
h1.textContent = meta.title;
document.getElementById('content').prepend(h1);
}
.state.error { color: #a3413a; }
// Meta line: date (and tags, if present)
const metaEl = document.getElementById('meta');
const parts = [];
if (meta.date) parts.push(formatDate(meta.date));
if (meta.tags) parts.push(meta.tags.split(',').map(t => t.trim()).join(' &middot; '));
if (parts.length) {
metaEl.innerHTML = parts.join(' &nbsp;/&nbsp; ');
metaEl.hidden = false;
}
} catch (err) {
showError('Could not load this post. It may have been moved or deleted.');
}
}
@media (prefers-reduced-motion: no-preference) {
.wrap { animation: rise 0.35s ease-out both; }
@keyframes rise {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
}
</style>
</head>
<body>
<div class="wrap">
<a class="back-link" href="index.html">&larr; All posts</a>
<p class="meta" id="meta" hidden></p>
<div id="content"><p class="state">Loading post&hellip;</p></div>
</div>
<script>
function parseFrontMatter(raw) {
const match = raw.match(/^---\r?\n([\s\S]+?)\r?\n---\r?\n([\s\S]*)$/);
if (!match) return { meta: {}, body: raw };
const meta = {};
match[1].split(/\r?\n/).forEach(line => {
const idx = line.indexOf(':');
if (idx === -1) return;
const key = line.slice(0, idx).trim();
let value = line.slice(idx + 1).trim();
// strip optional surrounding quotes: title: "My Post"
value = value.replace(/^["'](.*)["']$/, '$1');
meta[key] = value;
});
return { meta, body: match[2] };
}
function formatDate(dateStr) {
if (!dateStr) return '';
const d = new Date(dateStr);
if (isNaN(d)) return dateStr; // fall back to raw string if unparseable
return d.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' });
}
function showError(message) {
document.getElementById('content').innerHTML =
`<p class="state error">${message}</p>`;
}
// ---- main ------------------------------------------------------------
marked.setOptions({
highlight: (code, lang) => {
try {
return lang
? hljs.highlight(code, { language: lang }).value
: hljs.highlightAuto(code).value;
} catch (e) {
return code;
}
}
});
async function loadPost() {
const params = new URLSearchParams(window.location.search);
const file = params.get('file');
if (!file) {
showError('No post specified. Link to this page as <code>post.html?file=your-post.md</code>.');
return;
}
// Guard against path traversal — keep the fetch confined to posts/
if (file.includes('..') || file.startsWith('/')) {
showError('Invalid post path.');
return;
}
try {
const res = await fetch('posts/' + file);
if (!res.ok) throw new Error('HTTP ' + res.status);
const raw = await res.text();
const { meta, body } = parseFrontMatter(raw);
const html = marked.parse(body);
document.getElementById('content').innerHTML = html;
// Title: front matter > first H1 in the doc > filename
let title = meta.title;
if (!title) {
const h1 = document.querySelector('#content h1');
title = h1 ? h1.textContent : file.replace(/\.md$/, '');
}
document.title = title;
// Inject an H1 if front matter supplied a title but the body didn't have one
if (meta.title && !document.querySelector('#content h1:first-child')) {
const h1 = document.createElement('h1');
h1.textContent = meta.title;
document.getElementById('content').prepend(h1);
}
// Meta line: date (and tags, if present)
const metaEl = document.getElementById('meta');
const parts = [];
if (meta.date) parts.push(formatDate(meta.date));
if (meta.tags) parts.push(meta.tags.split(',').map(t => t.trim()).join(' &middot; '));
if (parts.length) {
metaEl.innerHTML = parts.join(' &nbsp;/&nbsp; ');
metaEl.hidden = false;
}
} catch (err) {
showError('Could not load this post. It may have been moved or deleted.');
}
}
document.addEventListener('DOMContentLoaded', loadPost);
</script>
</body>
document.addEventListener('DOMContentLoaded', loadPost);
</script>
</body>
</html>