This commit is contained in:
@@ -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 = `
|
||||
|
||||
Reference in New Issue
Block a user