archiving
Some checks failed
Build Roam Site / build (push) Has been cancelled

This commit is contained in:
2026-06-03 14:12:58 +01:00
parent a758ae1793
commit ac1d3839ba
22 changed files with 1180 additions and 285 deletions

View File

@@ -17,6 +17,7 @@ document.addEventListener('DOMContentLoaded', () => {
initMediaFixes(document);
initReadingProgress();
hookSearchResults();
initHomeDashboard();
});
/* =========================================================
@@ -93,14 +94,14 @@ function rearrangeDOM() {
function buildNoteToolbar(toolbar) {
toolbar.innerHTML = `
<button class="toolbar-btn" id="tb-back" title="Go back" aria-label="Go back">\u2190 back</button>
<button class="toolbar-btn" id="tb-forward" title="Go forward" aria-label="Go forward">forward \u2192</button>
<button class="toolbar-btn toolbar-btn-icon" id="tb-back" title="Go back" aria-label="Go back">\u2190</button>
<button class="toolbar-btn toolbar-btn-icon" id="tb-forward" title="Go forward" aria-label="Go forward">\u2192</button>
<div class="toolbar-sep"></div>
<button class="toolbar-btn" id="tb-toc" title="Toggle table of contents">contents</button>
<button class="toolbar-btn toolbar-btn-icon" id="tb-toc" title="Toggle table of contents" aria-label="Toggle table of contents">\u2261</button>
<button class="toolbar-btn toolbar-btn-icon" id="tb-backlinks" title="Toggle backlinks panel" aria-label="Toggle backlinks panel">\u29C9</button>
<div class="toolbar-spacer"></div>
<span class="note-graph-pos" id="note-graph-pos"></span>
<div class="toolbar-sep"></div>
<button class="toolbar-btn" id="tb-backlinks" title="Toggle backlinks panel">backlinks</button>
<button class="toolbar-btn toolbar-btn-icon" id="tb-close-all" title="Close all notes" aria-label="Close all notes">\u2715</button>
`;
}
@@ -108,19 +109,49 @@ function buildNoteToolbar(toolbar) {
SIDEBAR RAIL
========================================================= */
const RECENT_KEY = 'zg-recent-notes';
const RECENT_MAX = 12;
const FOLDER_STATE_KEY = 'zg-folder-state';
let sidebarDataCache = null;
async function fetchSidebarData() {
if (sidebarDataCache) return sidebarDataCache;
try {
const res = await fetch(`/assets/sidebar-tree.json?v=${Date.now()}`, { cache: 'no-store' });
if (res.ok) sidebarDataCache = await res.json();
} catch (_) {}
return sidebarDataCache || { mocs: [], tree: [] };
}
function closeSidebar() {
const sidebar = document.getElementById('sidebar');
if (!sidebar) return;
sidebar.classList.remove('expanded', 'mobile-open');
sidebar.dataset.activePanel = '';
sidebar.querySelectorAll('.rail-btn').forEach(b => b.classList.remove('active'));
document.getElementById('sidebar-backdrop')?.classList.remove('visible');
}
function initSidebarRail() {
const sidebar = document.getElementById('sidebar');
if (!sidebar) return;
const rail = el('div', { id: 'sidebar-rail', class: 'sidebar-rail' });
const body = el('div', { id: 'sidebar-body', class: 'sidebar-body' });
sidebar.append(rail, body);
const railBtns = [
{ label: '\u2630', title: 'All notes', id: 'rail-tree', panel: 'tree' },
{ label: '\u2315', title: 'Search', id: 'rail-search', action: 'search' },
{ label: '\u2B21', title: 'Atlas', id: 'rail-atlas', panel: 'atlas' },
{ label: '\u25F7', title: 'Recent', id: 'rail-recent', panel: 'recent' },
{ label: '\u2630', title: 'Browse', id: 'rail-browse', panel: 'browse' },
{ label: '\u2315', title: 'Search', id: 'rail-search', action: 'search' },
];
railBtns.forEach(({ label, title, id, panel, action }) => {
const btn = el('button', { class: 'rail-btn', id, title, 'aria-label': title });
btn.textContent = label;
sidebar.appendChild(btn);
rail.appendChild(btn);
btn.addEventListener('click', () => {
if (action === 'search') {
@@ -131,13 +162,12 @@ function initSidebarRail() {
}
return;
}
const isExpanded = sidebar.classList.contains('expanded');
const samePanel = sidebar.dataset.activePanel === panel;
if (isExpanded && samePanel) {
sidebar.classList.remove('expanded');
sidebar.dataset.activePanel = '';
btn.classList.remove('active');
closeSidebar();
} else {
sidebar.classList.add('expanded');
sidebar.dataset.activePanel = panel;
@@ -148,57 +178,142 @@ function initSidebarRail() {
});
});
// Mobile backdrop
let backdrop = document.getElementById('sidebar-backdrop');
if (!backdrop) {
backdrop = el('div', { id: 'sidebar-backdrop' });
document.body.appendChild(backdrop);
}
backdrop.addEventListener('click', () => {
sidebar.classList.remove('expanded', 'mobile-open');
backdrop.classList.remove('visible');
sidebar.querySelectorAll('.rail-btn').forEach(b => b.classList.remove('active'));
});
backdrop.addEventListener('click', closeSidebar);
initSidebarToggle();
}
function showSidebarPanel(panel) {
const sidebar = document.getElementById('sidebar');
sidebar.querySelectorAll('.sidebar-panel').forEach(p => p.remove());
const body = document.getElementById('sidebar-body');
if (!body) return;
body.innerHTML = '';
if (panel === 'tree') {
const panelEl = el('div', { class: 'sidebar-panel' });
panelEl.innerHTML = `
<div class="sidebar-header">
<span class="sidebar-header-label">all notes</span>
<button class="sidebar-close-btn" aria-label="Close sidebar">\u00D7</button>
</div>
<div id="file-tree"></div>
`;
sidebar.appendChild(panelEl);
const panelEl = el('div', { class: 'sidebar-panel', 'data-panel': panel });
panelEl.innerHTML = `
<div class="sidebar-header">
<span class="sidebar-header-label">${panel}</span>
<button class="sidebar-close-btn" aria-label="Close sidebar">\u00D7</button>
</div>
<div class="sidebar-panel-body" id="sidebar-panel-body"></div>
`;
body.appendChild(panelEl);
panelEl.querySelector('.sidebar-close-btn').addEventListener('click', () => {
sidebar.classList.remove('expanded');
sidebar.dataset.activePanel = '';
sidebar.querySelectorAll('.rail-btn').forEach(b => b.classList.remove('active'));
});
panelEl.querySelector('.sidebar-close-btn').addEventListener('click', closeSidebar);
buildFileTree(panelEl.querySelector('#file-tree'));
const container = panelEl.querySelector('#sidebar-panel-body');
if (panel === 'atlas') buildAtlasPanel(container);
else if (panel === 'recent') buildRecentPanel(container);
else if (panel === 'browse') buildBrowsePanel(container);
}
async function buildAtlasPanel(container) {
const data = await fetchSidebarData();
const mocs = data.mocs || [];
if (!mocs.length) {
container.innerHTML = '<p class="sidebar-empty">No MOC hubs found.</p>';
return;
}
const grid = el('div', { class: 'moc-grid' });
mocs.forEach(moc => {
const card = el('button', {
class: 'moc-card' + (moc.pinned ? ' moc-card-pinned' : ''),
type: 'button',
});
card.innerHTML = `
<span class="moc-card-title">${escHtml(moc.title)}</span>
${moc.pinned ? '<span class="moc-card-badge">start</span>' : ''}
`;
card.addEventListener('click', () => {
closeSidebar();
openNote(normalisePathname(moc.url), moc.title);
});
grid.appendChild(card);
});
container.appendChild(grid);
}
function getRecentNotes() {
try {
return JSON.parse(localStorage.getItem(RECENT_KEY) || '[]');
} catch (_) {
return [];
}
}
function trackRecentNote(pathname, title) {
if (!pathname) return;
const entry = { pathname, title, visitedAt: Date.now() };
let recent = getRecentNotes().filter(r => r.pathname !== pathname);
recent.unshift(entry);
localStorage.setItem(RECENT_KEY, JSON.stringify(recent.slice(0, RECENT_MAX)));
const panel = document.querySelector('.sidebar-panel[data-panel="recent"] #sidebar-panel-body');
if (panel) buildRecentPanel(panel);
}
function formatRelativeTime(ts) {
const diff = Date.now() - ts;
const mins = Math.floor(diff / 60000);
if (mins < 1) return 'just now';
if (mins < 60) return `${mins}m ago`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs}h ago`;
const days = Math.floor(hrs / 24);
if (days < 7) return `${days}d ago`;
return new Date(ts).toLocaleDateString();
}
function buildRecentPanel(container) {
const recent = getRecentNotes();
container.innerHTML = '';
if (!recent.length) {
container.innerHTML = '<p class="sidebar-empty">No notes visited yet.</p>';
return;
}
const list = el('div', { class: 'recent-list' });
recent.forEach(item => {
const row = el('button', { class: 'recent-item', type: 'button' });
row.innerHTML = `
<span class="recent-item-title">${escHtml(item.title)}</span>
<span class="recent-item-time">${formatRelativeTime(item.visitedAt)}</span>
`;
row.addEventListener('click', () => {
closeSidebar();
openNote(item.pathname, item.title);
});
list.appendChild(row);
});
container.appendChild(list);
}
function buildBrowsePanel(container) {
container.innerHTML = `
<div class="tree-filter-wrap">
<input type="search" class="tree-filter" placeholder="Filter notes\u2026" aria-label="Filter notes" />
</div>
<div id="file-tree"></div>
`;
const filterInput = container.querySelector('.tree-filter');
const treeRoot = container.querySelector('#file-tree');
filterInput.addEventListener('input', () => filterTree(treeRoot, filterInput.value.trim()));
buildFileTree(treeRoot);
}
async function buildFileTree(container) {
const currentPath = location.pathname;
let treeData = null;
try {
const res = await fetch(`/assets/sidebar-tree.json?v=${Date.now()}`, { cache: 'no-store' });
if (res.ok) {
const json = await res.json();
if (Array.isArray(json.tree)) treeData = json.tree;
}
} catch (_) {}
const data = await fetchSidebarData();
let treeData = Array.isArray(data.tree) ? data.tree : null;
if (treeData) {
renderTree(treeData, container, currentPath);
@@ -207,45 +322,120 @@ async function buildFileTree(container) {
const groups = await buildGroupsFromSearchIndex();
if (!groups?.length) {
const empty = el('div', { class: 'tree-file' });
empty.style.cssText = 'opacity:.4;font-size:.7rem;padding:8px 14px;';
empty.textContent = 'no notes found';
container.appendChild(empty);
container.innerHTML = '<p class="sidebar-empty">No notes found.</p>';
return;
}
const fallbackTree = groups.map(g => ({ label: g.label, children: [], files: g.files }));
renderTree(fallbackTree, container, currentPath);
}
function renderTree(nodes, container, currentPath) {
function getFolderState() {
try {
return JSON.parse(localStorage.getItem(FOLDER_STATE_KEY) || '{}');
} catch (_) {
return {};
}
}
function setFolderState(key, isOpen) {
const state = getFolderState();
state[key] = isOpen;
localStorage.setItem(FOLDER_STATE_KEY, JSON.stringify(state));
}
function countFilesInNode(node) {
let count = (node.files || []).length;
for (const child of node.children || []) {
count += countFilesInNode(child);
}
return count;
}
function nodeContainsPath(node, pathname, prefix = '') {
const folderKey = prefix ? `${prefix}/${node.label}` : node.label;
for (const f of node.files || []) {
if (normalisePathname(f.url) === pathname) return true;
}
for (const child of node.children || []) {
if (nodeContainsPath(child, pathname, folderKey)) return true;
}
return false;
}
function renderTree(nodes, container, currentPath, prefix = '') {
nodes.forEach(node => {
const folderEl = el('div', { class: 'tree-folder', 'data-folder': node.label });
const icon = el('span', { class: 'tree-icon' });
icon.textContent = '\u25BE ';
const folderKey = prefix ? `${prefix}/${node.label}` : node.label;
const fileCount = countFilesInNode(node);
const containsActive = nodeContainsPath(node, currentPath, prefix);
const folderState = getFolderState();
const defaultOpen = containsActive || folderState[folderKey] === true;
const isOpen = defaultOpen;
const folderEl = el('div', {
class: 'tree-folder',
'data-folder': node.label,
'data-folder-key': folderKey,
});
const icon = el('span', { class: 'tree-icon' });
icon.textContent = isOpen ? '\u25BE ' : '\u25B8 ';
const labelSpan = el('span', { class: 'tree-folder-label' });
labelSpan.textContent = node.label;
folderEl.append(icon, labelSpan);
const badge = el('span', { class: 'tree-folder-count' });
badge.textContent = String(fileCount);
folderEl.append(icon, labelSpan, badge);
const childrenWrap = el('div', { class: 'tree-children' });
if (!isOpen) childrenWrap.style.display = 'none';
const searchTerms = [node.label.toLowerCase()];
(node.files || []).forEach(f => {
const pathname = normalisePathname(f.url);
searchTerms.push(f.title.toLowerCase());
childrenWrap.appendChild(makeFileItem({ title: f.title, pathname }, currentPath));
});
if (node.children?.length) renderTree(node.children, childrenWrap, currentPath);
if (node.children?.length) {
renderTree(node.children, childrenWrap, currentPath, folderKey);
}
folderEl.addEventListener('click', () => {
const isOpen = childrenWrap.style.display !== 'none';
childrenWrap.style.display = isOpen ? 'none' : '';
icon.textContent = isOpen ? '\u25B8 ' : '\u25BE ';
const nowOpen = childrenWrap.style.display === 'none';
childrenWrap.style.display = nowOpen ? '' : 'none';
icon.textContent = nowOpen ? '\u25BE ' : '\u25B8 ';
setFolderState(folderKey, nowOpen);
});
folderEl.dataset.searchText = searchTerms.join(' ');
childrenWrap.dataset.searchText = searchTerms.join(' ');
container.appendChild(folderEl);
container.appendChild(childrenWrap);
});
}
function filterTree(container, query) {
const q = query.toLowerCase();
container.querySelectorAll('.tree-folder, .tree-children, .tree-file').forEach(el => {
if (!q) {
el.classList.remove('tree-filtered-out');
return;
}
const text = (el.dataset.searchText || el.textContent || '').toLowerCase();
el.classList.toggle('tree-filtered-out', !text.includes(q));
});
if (q) {
container.querySelectorAll('.tree-children').forEach(wrap => {
wrap.style.display = '';
const folder = wrap.previousElementSibling;
if (folder?.classList.contains('tree-folder')) {
const icon = folder.querySelector('.tree-icon');
if (icon) icon.textContent = '\u25BE ';
}
});
}
}
async function buildGroupsFromSearchIndex() {
try {
const res = await fetch('/search-index.json');
@@ -267,14 +457,17 @@ function makeFileItem({ title, pathname }, currentPath) {
const fileEl = el('div', {
class: 'tree-file' + (pathname === currentPath ? ' active' : ''),
'data-pathname': pathname,
'data-search-text': title.toLowerCase(),
});
const iconSpan = el('span', { class: 'tree-icon' });
iconSpan.textContent = '\u00B7 ';
const titleSpan = el('span', { class: 'tree-file-title' });
titleSpan.textContent = title;
fileEl.appendChild(iconSpan);
fileEl.appendChild(document.createTextNode(title));
fileEl.appendChild(titleSpan);
fileEl.title = title;
fileEl.addEventListener('click', () => {
document.getElementById('sidebar')?.classList.remove('expanded');
closeSidebar();
openNote(pathname, title);
});
return fileEl;
@@ -378,6 +571,7 @@ history.replaceState(
document.getElementById('tb-forward')?.addEventListener('click', goForward);
document.getElementById('tb-backlinks')?.addEventListener('click', toggleBacklinkPanel);
document.getElementById('tb-toc')?.addEventListener('click', toggleTOC);
document.getElementById('tb-close-all')?.addEventListener('click', closeAllNotes);
// Backlink panel close
document.getElementById('bp-close')?.addEventListener('click', () => {
@@ -548,6 +742,11 @@ function activateNote(id, skipBrowserWrite = true) {
const area = document.getElementById('content-area');
if (area) area.scrollTop = 0;
const current = noteHistory[historyIndex];
if (current) {
trackRecentNote(current.pathname, current.label);
}
updateBreadcrumb();
updateToolbar();
@@ -567,6 +766,18 @@ function goForward() {
window.history.forward();
}
function closeAllNotes() {
if (noteHistory.length <= 1) return;
const root = noteHistory[0];
noteHistory.slice(1).forEach(n => {
if (!n.isRoot) n.paneEl.remove();
});
noteHistory.splice(1);
historyIndex = 0;
activateNote(root.id, true);
history.replaceState(makeHistoryState(root), '', historyUrlFor(root));
}
function updateBreadcrumb() {
const trail = document.getElementById('breadcrumb-trail');
if (!trail) return;
@@ -934,9 +1145,9 @@ function initSidebarToggle() {
e.preventDefault();
const nowExpanded = sidebar.classList.toggle('expanded');
if (nowExpanded && !sidebar.dataset.activePanel) {
sidebar.dataset.activePanel = 'tree';
showSidebarPanel('tree');
sidebar.querySelector('#rail-tree')?.classList.add('active');
sidebar.dataset.activePanel = 'atlas';
showSidebarPanel('atlas');
sidebar.querySelector('#rail-atlas')?.classList.add('active');
}
if (!nowExpanded) {
sidebar.dataset.activePanel = '';
@@ -1285,6 +1496,51 @@ function normalisePathname(p) {
// Lazy sidebar builder stub (built on demand)
async function buildSidebar() {}
async function initHomeDashboard() {
const pathname = normalisePathname(location.pathname);
if (pathname !== '/' && !pathname.endsWith('/index.html')) return;
const content = document.querySelector('#content');
if (!content || content.querySelector('.home-dashboard')) return;
const data = await fetchSidebarData();
const mocs = data.mocs || [];
if (!mocs.length) return;
const dash = el('div', { class: 'home-dashboard' });
dash.innerHTML = `
<header class="home-hero">
<h2 class="home-hero-title">Zettelgarten</h2>
<p class="home-hero-sub">A private library of notes, maps, and wandering thoughts.</p>
</header>
<div class="home-quick-links">
<button type="button" class="home-quick-link" data-action="search">Search <kbd>/</kbd></button>
<a class="home-quick-link" href="/all-files.html">All files</a>
</div>
<h3 class="home-section-label">Start here</h3>
<div class="home-moc-grid"></div>
`;
const grid = dash.querySelector('.home-moc-grid');
mocs.forEach(moc => {
const card = el('a', {
class: 'moc-card home-moc-card' + (moc.pinned ? ' moc-card-pinned' : ''),
href: moc.url,
});
card.innerHTML = `
<span class="moc-card-title">${escHtml(moc.title)}</span>
${moc.pinned ? '<span class="moc-card-badge">start</span>' : ''}
`;
grid.appendChild(card);
});
dash.querySelector('[data-action="search"]')?.addEventListener('click', () => {
window.openSearchModal?.();
});
content.insertBefore(dash, content.firstChild);
}
// Pane-out animation keyframe
const _s = document.createElement('style');
_s.textContent = `

View File

@@ -88,14 +88,12 @@
========================================================= */
@media (max-width: 900px) {
#sidebar {
width: 0;
border-right: none;
overflow: hidden;
#sidebar:not(.expanded):not(.mobile-open) {
width: var(--rail-w);
}
.fullwidth {
max-width: calc(100vw - 2rem);
max-width: calc(100vw - var(--rail-w) - 2rem);
}
}
@@ -172,22 +170,22 @@
position: fixed;
top: 0;
left: 0;
width: 80vw;
max-width: 300px;
width: min(92vw, var(--sidebar-w));
height: 100dvh;
z-index: 400;
border-right: 1px solid var(--border-mid);
overflow-y: auto;
overflow-x: hidden;
overflow: hidden;
transform: translateX(-100%);
transition: transform var(--dur-mid) var(--ease-out),
box-shadow var(--dur-mid) var(--ease-out);
box-shadow: none;
}
#sidebar.mobile-open {
#sidebar.mobile-open,
#sidebar.expanded {
transform: translateX(0);
box-shadow: 4px 0 32px rgba(0, 0, 0, 0.5);
width: min(92vw, var(--sidebar-w)) !important;
}
#sidebar-backdrop {
@@ -218,12 +216,7 @@
#content-area {
overflow: visible;
height: auto;
min-height: calc(100dvh - var(--header-h) - var(--tab-h));
background-attachment: scroll;
}
#content-area::before {
display: none;
min-height: calc(100dvh - var(--header-h) - var(--toolbar-h));
}
/* ---- Note content ---- */

View File

@@ -2,7 +2,7 @@
ZETTELKASTEN GARDEN style.css
========================================================= */
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,500;0,600;1,500&family=DM+Mono:wght@400;500&family=Inter:wght@400;500;600&family=Noto+Sans+Arabic:wght@400;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,500;0,600;1,500&family=DM+Mono:wght@400;500&family=Spectral:ital,wght@0,300;0,400;0,500;1,300;1,400&family=Noto+Sans+Arabic:wght@400;700&display=swap');
/* =========================================================
TOKENS
@@ -10,25 +10,28 @@
:root {
--header-h: 48px;
--toolbar-h: 36px;
--rail-w: 52px;
--panel-w: 260px;
--content-max: none;
--content-pad: clamp(1.5rem, 4vw, 4.5rem);
--toolbar-h: 32px;
--rail-w: 48px;
--panel-w: 280px;
--sidebar-w: calc(var(--rail-w) + var(--panel-w));
--content-max: min(72rem, calc(100vw - var(--rail-w) - 2.5rem));
--content-pad: clamp(1.25rem, 2.5vw, 2.25rem);
/* Dark academia: ink, vellum, forest green, oxblood, and aged brass */
--bg: #11100c;
--page-bg: #090907;
--pane-bg: #15130f;
--surface: #1e1a14;
--surface-2: #29231a;
/* Scriptorium: ink, vellum, oxblood, antique gold */
--bg: #0f0e0b;
--page-bg: #070605;
--pane-bg: #0a0908;
--reading-surface: #12100d;
--surface: #1a1712;
--surface-2: #242019;
--fg: #e8dfcd;
--fg-dim: #b5a68f;
--fg-faint: #766a58;
--fg-ghost: #43382b;
--accent: #b68b45;
--accent: #c4a35a;
--accent-2: #6b2d3c;
--accent-dim: color-mix(in oklab, var(--accent) 35%, transparent);
--accent-sub: color-mix(in oklab, var(--accent) 10%, transparent);
--accent-glow:color-mix(in oklab, var(--accent) 6%, transparent);
@@ -38,13 +41,16 @@
--heading-3: #a8604f;
--link: #c7a763;
--link-hover:#f1e2c2;
--tag-bg: #251f16;
--tag-bg: #1f1a14;
--tag-fg: var(--accent);
--border: #332c20;
--border: #2e2820;
--border-2: #4a3d29;
--code-bg: #0c0c09;
--border-mid: #3a3228;
--chip-bg: var(--surface);
--code-bg: #080807;
--font-body: 'Inter', 'Noto Sans Arabic', system-ui, sans-serif;
--font-body: 'Spectral', 'Noto Sans Arabic', Georgia, serif;
--font-ui: system-ui, -apple-system, sans-serif;
--font-mono: 'DM Mono', 'Fira Mono', monospace;
--font-serif: 'Cormorant Garamond', Georgia, serif;
@@ -58,24 +64,28 @@
--r4:1rem; --r5:1.5rem; --r6:2rem; --r8:3rem;
}
/* Light theme */
/* Light theme — daylight reading room */
[data-theme="light"] {
--bg: #f3eee3;
--page-bg: #e9e0cf;
--pane-bg: #f7f1e6;
--bg: #f5f0e6;
--page-bg: #ebe4d6;
--pane-bg: #f0ebe1;
--reading-surface: #f7f1e6;
--surface: #efe5d4;
--surface-2: #e3d5be;
--fg: #211c15;
--fg-dim: #625744;
--fg-faint: #998b72;
--fg-ghost: #d2c3a8;
--accent: #80602f;
--fg: #1c1812;
--fg-dim: #5c5040;
--fg-faint: #8a7d68;
--fg-ghost: #c8baa3;
--accent: #8a6b32;
--accent-2: #7e3a45;
--border: #d4c4a7;
--border-2: #b9a57f;
--border-mid:#c4b08e;
--chip-bg: #e8dcc8;
--code-bg: #e8dcc8;
--tag-bg: #dfd0b5;
--link: #684f28;
--heading: #211c15;
--heading: #1c1812;
--heading-2: #5b4929;
--heading-3: #7e4438;
}
@@ -130,11 +140,32 @@ body, p, li, td, th, span, div {
.banner-logo {
height: 26px; width: 26px;
border-radius: 50%; object-fit: cover;
opacity: 0.85; flex-shrink: 0;
opacity: 0.9; flex-shrink: 0;
transition: opacity var(--dur-fast);
}
.banner-logo:hover { opacity: 1; }
.banner-brand {
display: inline-flex;
align-items: center;
gap: 0.55rem;
color: var(--heading);
text-decoration: none;
border: none;
padding: 0;
flex-shrink: 0;
}
.banner-brand:hover {
color: var(--accent);
background: transparent;
}
.banner-title {
font-family: var(--font-serif);
font-size: 1.15rem;
font-weight: 500;
letter-spacing: 0.02em;
}
/* Breadcrumb trail */
#breadcrumb-trail {
display: flex;
@@ -191,7 +222,7 @@ body, p, li, td, th, span, div {
height: 30px;
padding: 0 0.45rem 0 0.7rem;
font-size: 0.78rem;
font-family: var(--font-body);
font-family: var(--font-ui);
color: var(--fg-dim);
background: var(--surface);
border: 1px solid var(--border);
@@ -390,14 +421,13 @@ body, p, li, td, th, span, div {
width: var(--rail-w);
flex-shrink: 0;
background:
linear-gradient(90deg, color-mix(in oklab, #233327 18%, transparent), transparent 78%),
linear-gradient(180deg, color-mix(in oklab, var(--accent-2) 8%, transparent), transparent 40%),
linear-gradient(90deg, color-mix(in oklab, #1a2e1f 22%, transparent), transparent 90%),
var(--bg);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
align-items: center;
padding: var(--r3) 0;
gap: var(--r2);
flex-direction: row;
align-items: stretch;
overflow: hidden;
transition: width var(--dur-mid) var(--ease-out);
z-index: 100;
@@ -405,29 +435,60 @@ body, p, li, td, th, span, div {
}
#sidebar.expanded {
width: var(--panel-w);
align-items: flex-start;
overflow-y: auto;
width: var(--sidebar-w);
}
.sidebar-rail {
width: var(--rail-w);
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;
padding: var(--r3) 0;
gap: var(--r2);
border-right: 1px solid color-mix(in oklab, var(--accent) 12%, var(--border));
background: color-mix(in oklab, var(--surface) 40%, transparent);
}
.sidebar-body {
display: none;
flex: 1;
min-width: 0;
overflow: hidden;
}
#sidebar.expanded .sidebar-body {
display: flex;
flex-direction: column;
}
/* Rail icon buttons */
.rail-btn {
width: 36px; height: 36px;
border-radius: 8px;
width: 34px; height: 34px;
border-radius: 6px;
background: transparent;
border: none;
border: 1px solid transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--fg-faint);
font-size: 1.05rem;
font-size: 1rem;
flex-shrink: 0;
transition: color var(--dur-fast), background var(--dur-fast);
transition: color var(--dur-fast), background var(--dur-fast), border-color var(--dur-fast), box-shadow var(--dur-fast);
position: relative;
}
.rail-btn:hover { color: var(--fg); background: var(--accent-sub); }
.rail-btn.active { color: var(--accent); background: var(--accent-glow); }
.rail-btn:hover {
color: var(--fg);
background: var(--accent-sub);
border-color: color-mix(in oklab, var(--accent) 20%, transparent);
}
.rail-btn.active {
color: var(--accent);
background: var(--accent-glow);
border-color: var(--accent-dim);
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--accent) 15%, transparent);
}
.rail-btn[title]:not(:disabled):hover::after {
content: attr(title);
@@ -447,8 +508,31 @@ body, p, li, td, th, span, div {
z-index: 500;
}
.sidebar-panel { display: none; width: 100%; flex-direction: column; }
#sidebar.expanded .sidebar-panel { display: flex; }
.sidebar-panel {
display: flex;
width: 100%;
flex: 1;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.sidebar-panel-body {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding-bottom: 12px;
scrollbar-width: thin;
scrollbar-color: var(--border-2) transparent;
}
.sidebar-empty {
padding: 1.25rem 14px;
color: var(--fg-faint);
font-size: 0.78rem;
font-style: italic;
text-align: center;
}
.sidebar-header {
display: flex;
@@ -481,7 +565,133 @@ body, p, li, td, th, span, div {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding-bottom: 12px;
padding-bottom: 4px;
}
.tree-filter-wrap {
padding: 6px 10px 8px;
flex-shrink: 0;
}
.tree-filter {
width: 100%;
padding: 5px 8px;
font-size: 0.75rem;
font-family: var(--font-ui);
color: var(--fg);
background: var(--surface);
border: 1px solid var(--border);
border-radius: 4px;
outline: none;
transition: border-color var(--dur-fast), box-shadow var(--dur-fast);
}
.tree-filter:focus {
border-color: var(--accent-dim);
box-shadow: 0 0 0 2px var(--accent-sub);
}
.tree-filter::placeholder { color: var(--fg-faint); }
.tree-filtered-out { display: none !important; }
/* MOC cards (Atlas + home) */
.moc-grid,
.home-moc-grid {
display: flex;
flex-direction: column;
gap: 6px;
padding: 4px 8px 8px;
}
.moc-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
width: 100%;
padding: 8px 10px;
text-align: left;
background: var(--surface);
border: 1px solid var(--border);
border-left: 2px solid color-mix(in oklab, var(--accent) 50%, transparent);
border-radius: 4px;
cursor: pointer;
color: var(--fg);
text-decoration: none;
transition: transform var(--dur-fast), border-color var(--dur-fast), background var(--dur-fast), box-shadow var(--dur-fast);
}
.moc-card:hover {
background: var(--surface-2);
border-color: var(--accent-dim);
border-left-color: var(--accent);
transform: translateY(-1px);
box-shadow: 0 4px 16px color-mix(in oklab, var(--accent) 8%, transparent);
}
.moc-card-pinned {
border-left-color: var(--accent);
background: color-mix(in oklab, var(--accent) 5%, var(--surface));
}
.moc-card-title {
font-family: var(--font-serif);
font-size: 0.88rem;
font-weight: 500;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.moc-card-badge {
flex-shrink: 0;
font-family: var(--font-mono);
font-size: 0.55rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--accent);
padding: 2px 5px;
border: 1px solid var(--accent-dim);
border-radius: 3px;
}
/* Recent list */
.recent-list {
display: flex;
flex-direction: column;
gap: 2px;
padding: 4px 6px;
}
.recent-item {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
width: 100%;
padding: 7px 10px;
text-align: left;
background: transparent;
border: 1px solid transparent;
border-radius: 4px;
cursor: pointer;
color: var(--fg-dim);
transition: background var(--dur-fast), color var(--dur-fast), border-color var(--dur-fast);
}
.recent-item:hover {
background: var(--accent-sub);
color: var(--fg);
border-color: color-mix(in oklab, var(--accent) 15%, transparent);
}
.recent-item-title {
font-size: 0.78rem;
line-height: 1.35;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
.recent-item-time {
font-family: var(--font-mono);
font-size: 0.62rem;
color: var(--fg-faint);
letter-spacing: 0.02em;
}
/* Firefox */
@@ -512,7 +722,7 @@ body, p, li, td, th, span, div {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 10px 4px 12px;
padding: 4px 8px 4px 10px;
font-size: 0.68rem;
cursor: pointer;
color: var(--fg-faint);
@@ -520,32 +730,43 @@ body, p, li, td, th, span, div {
margin: 2px 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
user-select: none;
font-weight: 500;
font-family: var(--font-mono);
letter-spacing: 0.06em;
letter-spacing: 0.04em;
text-transform: uppercase;
transition: background var(--dur-fast), color var(--dur-fast);
}
.tree-folder:hover { background: var(--accent-sub); color: var(--fg); }
.tree-folder-count {
margin-left: auto;
font-size: 0.58rem;
color: var(--fg-ghost);
flex-shrink: 0;
padding-left: 4px;
}
.tree-file {
/* display: flex; */
display: flex;
align-items: center;
gap: 6px;
padding: 3px 10px 3px 12px;
padding: 3px 8px 3px 10px;
font-size: 0.76rem;
cursor: pointer;
color: var(--fg-dim);
border-radius: 3px;
margin: 1px 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
user-select: none;
transition: background var(--dur-fast), color var(--dur-fast);
}
.tree-file-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
.tree-file:hover { background: var(--accent-sub); color: var(--fg); }
.tree-file.active {
background: color-mix(in oklab, var(--accent) 14%, transparent);
@@ -578,10 +799,10 @@ body, p, li, td, th, span, div {
#note-toolbar {
display: flex;
align-items: center;
gap: var(--r2);
padding: 4px var(--r4);
gap: var(--r1);
padding: 2px var(--r3);
height: var(--toolbar-h);
background: color-mix(in oklab, var(--bg) 92%, #233327 8%);
background: transparent;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
font-size: 0.75rem;
@@ -591,6 +812,7 @@ body, p, li, td, th, span, div {
.toolbar-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
padding: 3px 8px;
border-radius: 4px;
@@ -603,6 +825,12 @@ body, p, li, td, th, span, div {
transition: all var(--dur-fast);
white-space: nowrap;
}
.toolbar-btn-icon {
width: 26px;
height: 26px;
padding: 0;
font-size: 0.85rem;
}
.toolbar-btn:hover:not(:disabled) {
border-color: var(--border-2);
color: var(--fg);
@@ -623,10 +851,11 @@ body, p, li, td, th, span, div {
.toolbar-spacer { flex: 1; }
.note-graph-pos {
color: var(--fg-faint);
font-size: 0.68rem;
letter-spacing: 0.03em;
max-width: 200px;
color: var(--fg-ghost);
font-size: 0.62rem;
letter-spacing: 0.04em;
font-style: italic;
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -647,36 +876,45 @@ body, p, li, td, th, span, div {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
background-color: var(--pane-bg);
background-image:
linear-gradient(180deg, color-mix(in oklab, #233327 12%, transparent), transparent 22rem),
linear-gradient(90deg, color-mix(in oklab, var(--accent) 7%, transparent) 0 1px, transparent 1px),
repeating-linear-gradient(
180deg,
transparent, transparent 27px,
color-mix(in oklab, var(--fg) 2%, transparent) 27px,
color-mix(in oklab, var(--fg) 2%, transparent) 28px
);
background:
radial-gradient(ellipse 80% 60% at 50% 0%, color-mix(in oklab, var(--accent) 4%, transparent), transparent 70%),
var(--pane-bg);
position: relative;
scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
background-attachment: local;
}
#content-area::-webkit-scrollbar { width: 5px; }
#content-area::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
#content-area::before {
content: '';
position: absolute;
top: 0; left: 52px; width: 1px; height: 100%;
background: color-mix(in oklab, var(--accent) 8%, transparent);
pointer-events: none;
z-index: 0;
.tab-content {
display: none;
min-height: 100%;
position: relative;
z-index: 1;
padding: var(--r4) var(--r2) var(--r6);
}
#sidebar.expanded ~ #main .tab-content,
body:has(#sidebar.expanded) .tab-content {
--content-max: min(68rem, calc(100vw - var(--sidebar-w) - 2rem));
}
.tab-content { display: none; min-height: 100%; position: relative; z-index: 1; }
.tab-content.active { display: block; }
.tab-content > #content,
.tab-content > .tab-content-inner {
margin: 0 auto;
width: 100%;
max-width: var(--content-max);
padding: var(--r6) var(--content-pad) calc(var(--r8) * 1.5);
background: var(--reading-surface);
border: 1px solid var(--border);
border-top: 1px solid color-mix(in oklab, var(--accent) 25%, var(--border));
border-radius: 2px;
box-shadow:
0 1px 0 color-mix(in oklab, var(--accent) 8%, transparent),
0 24px 64px color-mix(in oklab, #000 35%, transparent);
}
/* =========================================================
NOTE CONTENT
========================================================= */
@@ -685,7 +923,89 @@ body, p, li, td, th, span, div {
margin: 0 auto;
width: 100%;
max-width: var(--content-max);
padding: var(--r6) var(--content-pad) calc(var(--r8) * 2.5);
}
/* =========================================================
HOME DASHBOARD
========================================================= */
.home-dashboard {
margin-bottom: var(--r8);
padding-bottom: var(--r6);
border-bottom: 1px solid var(--border);
}
.home-hero-title {
font-family: var(--font-serif);
font-size: 2rem;
font-weight: 500;
color: var(--heading);
margin: 0 0 var(--r2);
letter-spacing: 0.02em;
}
.home-hero-sub {
margin: 0 0 var(--r5);
color: var(--fg-dim);
font-size: 1rem;
font-style: italic;
max-width: 36ch;
}
.home-quick-links {
display: flex;
flex-wrap: wrap;
gap: var(--r2);
margin-bottom: var(--r6);
}
.home-quick-link {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 5px 12px;
font-size: 0.75rem;
font-family: var(--font-mono);
color: var(--fg-dim);
background: var(--surface);
border: 1px solid var(--border);
border-radius: 4px;
text-decoration: none;
cursor: pointer;
transition: border-color var(--dur-fast), color var(--dur-fast), background var(--dur-fast);
}
.home-quick-link:hover {
color: var(--accent);
border-color: var(--accent-dim);
background: var(--accent-glow);
}
.home-quick-link kbd {
font-size: 0.65rem;
padding: 1px 4px;
border: 1px solid var(--border-2);
border-radius: 3px;
color: var(--fg-faint);
}
.home-section-label {
font-family: var(--font-mono);
font-size: 0.62rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--fg-faint);
margin: 0 0 var(--r3);
font-weight: 500;
}
.home-moc-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
gap: 8px;
padding: 0;
}
.home-moc-card {
text-decoration: none;
}
/* =========================================================
@@ -1076,8 +1396,8 @@ li ul li::before { content: "\00B7"; font-weight: normal; color: var(--fg-faint)
blockquote {
margin: var(--r5) 0;
padding: var(--r4) var(--r5);
border-left: 2px solid var(--accent);
background: var(--accent-glow);
border-left: 3px solid var(--accent-2);
background: color-mix(in oklab, var(--accent-2) 6%, transparent);
border-radius: 0 6px 6px 0;
color: var(--fg-dim);
font-family: var(--font-serif);
@@ -1383,15 +1703,16 @@ pre.src {
========================================================= */
#postamble, footer {
color: var(--fg-faint);
color: var(--fg-ghost);
padding: var(--r2) var(--r5);
border-top: 1px solid var(--border);
background: var(--bg);
border-top: 1px solid color-mix(in oklab, var(--border) 60%, transparent);
background: var(--page-bg);
text-align: center;
font-size: 0.68rem;
font-size: 0.62rem;
font-family: var(--font-mono);
letter-spacing: 0.03em;
flex-shrink: 0;
opacity: 0.7;
}
#postamble a, footer a { color: var(--fg-faint); }
#postamble a:hover, footer a:hover { color: var(--fg); }
@@ -1433,7 +1754,7 @@ pre.src {
@media (max-width: 768px) {
:root {
--rail-w: 0px;
--rail-w: 48px;
--content-pad: 1rem;
}
#sidebar {
@@ -1445,14 +1766,26 @@ pre.src {
width: 0;
}
#sidebar.mobile-open,
#sidebar.expanded { width: var(--panel-w) !important; }
#sidebar.expanded { width: min(92vw, var(--sidebar-w)) !important; }
#backlink-panel.open { width: 200px; }
#breadcrumb-trail { display: none; }
#note-toolbar { flex-wrap: wrap; height: auto; }
#note-toolbar { flex-wrap: wrap; height: auto; min-height: var(--toolbar-h); }
/* Code blocks on mobile */
.tab-content {
padding: var(--r3) var(--r2) var(--r6);
}
.tab-content > #content,
.tab-content > .tab-content-inner {
padding: var(--r4) var(--content-pad) var(--r6);
box-shadow: 0 8px 32px color-mix(in oklab, #000 25%, transparent);
}
.home-moc-grid {
grid-template-columns: 1fr;
}
.org-src-container > pre,
pre.src {
font-size: 0.75rem;