From a2c66a98dc823f8d2e66110124775660073f0816 Mon Sep 17 00:00:00 2001 From: Mark Randall Havens Date: Sun, 9 Nov 2025 18:43:39 +0000 Subject: [PATCH] regress and update html render --- package.json | 2 +- public/app.js | 159 +++++++++++++++++++-------------------- public/index.html | 105 +++++++++++++------------- public/styles.css | 52 ++++++------- tools/generate-index.mjs | 20 ++--- 5 files changed, 167 insertions(+), 171 deletions(-) diff --git a/package.json b/package.json index 9e3d97f..93321a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "the-fold-within", - "version": "3.2.0", + "version": "3.3.1", "dependencies": { "pdf-parse": "^1.1.1" } diff --git a/public/app.js b/public/app.js index 829d184..b89a022 100755 --- a/public/app.js +++ b/public/app.js @@ -1,3 +1,9 @@ +/** + * app.js – v3.3.1 PREVIEW + PORTAL + * High-coherence, readable, maintainable. + * No hacks. No surgery. Only truth. + */ + const els = { menuBtn: document.getElementById("menuBtn"), primaryNav: document.getElementById("primaryNav"), @@ -17,12 +23,13 @@ const els = { let indexData = null; let sidebarOpen = false; let currentParent = null; -let indexFiles = null; // Cached +let indexFiles = null; // Cached index files +// === INITIALIZATION === async function init() { try { indexData = await (await fetch("index.json")).json(); - indexFiles = indexData.flat.filter(f => f.isIndex); // Cache + indexFiles = indexData.flat.filter(f => f.isIndex); populateNav(); populateSections(); populateTags(); @@ -35,6 +42,7 @@ async function init() { } } +// === NAVIGATION === function populateNav() { els.primaryNav.innerHTML = 'Home'; const navSections = [...new Set( @@ -55,11 +63,8 @@ function populateSections() { els.sectionSelect.appendChild(opt); }); - if (indexData.sections.includes("posts")) { - els.sectionSelect.value = "posts"; - } else if (indexData.sections.length > 0) { - els.sectionSelect.value = indexData.sections[0]; - } + const defaultSection = indexData.sections.includes("posts") ? "posts" : indexData.sections[0]; + if (defaultSection) els.sectionSelect.value = defaultSection; } function populateTags() { @@ -70,11 +75,7 @@ function populateTags() { }); } -function formatTimestamp(ms) { - const d = new Date(ms); - return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`; -} - +// === UI WIRING === function wireUI() { els.menuBtn.addEventListener("click", () => { sidebarOpen = !sidebarOpen; @@ -95,6 +96,7 @@ function wireUI() { [els.tagSelect, els.sortSelect, els.searchMode].forEach(el => el.addEventListener("change", renderList)); els.searchBox.addEventListener("input", renderList); + // Close sidebar on content click (mobile) els.content.addEventListener("click", (e) => { if (window.innerWidth < 1024 && document.body.classList.contains("sidebar-open")) { if (!e.target.closest("#sidebar")) { @@ -105,6 +107,7 @@ function wireUI() { }); } +// === LIST RENDERING === function renderList() { const section = els.sectionSelect.value; const tags = Array.from(els.tagSelect.selectedOptions).map(o => o.value.toLowerCase()); @@ -127,7 +130,7 @@ function renderList() { posts.forEach(p => { const li = document.createElement("li"); const pin = p.isPinned ? "Star " : ""; - const time = formatTimestamp(p.ctime); + const time = new Date(p.ctime).toLocaleDateString(); li.innerHTML = `${pin}${p.title}${time}`; els.postList.appendChild(li); }); @@ -143,7 +146,7 @@ function loadDefaultForSection(section) { location.hash = `#/${pinned.path}`; } -// NESTED HORIZON: Deep-Aware Sub-Navigation +// === SUBNAV (NESTED HORIZON) === function renderSubNav(parent) { const subnav = els.subNav; subnav.innerHTML = ""; @@ -159,16 +162,14 @@ function renderSubNav(parent) { subnav.appendChild(link); }); - requestAnimationFrame(() => { - subnav.classList.add("visible"); - }); + requestAnimationFrame(() => subnav.classList.add("visible")); } +// === HASH ROUTING === async function handleHash() { els.viewer.innerHTML = ""; const rel = location.hash.replace(/^#\//, ""); const parts = rel.split("/").filter(Boolean); - const currentParentPath = parts.slice(0, -1).join("/") || parts[0] || null; if (currentParentPath !== currentParent) { @@ -186,41 +187,28 @@ async function handleHash() { if (rel.endsWith('/')) { const currentPath = parts.join("/"); - const indexFile = indexFiles.find(f => { const dir = f.path.split("/").slice(0, -1).join("/"); return dir === currentPath; }); if (indexFile) { - try { - if (indexFile.ext === ".md") { - const src = await fetch(indexFile.path).then(r => r.ok ? r.text() : ""); - const html = marked.parse(src || `# ${currentPath.split("/").pop()}\n\nNo content yet.`); - els.viewer.innerHTML = `
${html}
`; - } else { - renderIframe("/" + indexFile.path); - } - } catch (e) { - els.viewer.innerHTML = `

${currentPath.split("/").pop()}

No content yet.

`; + if (indexFile.ext === ".md") { + await renderMarkdown(indexFile.path); + } else { + await renderIframe("/" + indexFile.path); } } else { - if (topSection) { - els.sectionSelect.value = topSection; - renderList(); - loadDefaultForSection(topSection); - } else { - els.viewer.innerHTML = `

${currentPath.split("/").pop()}

No content yet.

`; - } + if (topSection) loadDefaultForSection(topSection); + else els.viewer.innerHTML = `

${currentPath.split("/").pop()}

No content yet.

`; } - } - else { + } else { const file = indexData.flat.find(f => f.path === rel); if (!file) { els.viewer.innerHTML = "

404

Not found.

"; return; } - file.ext === ".md" ? await renderMarkdown(file.path) : renderIframe("/" + file.path); + file.ext === ".md" ? await renderMarkdown(file.path) : await renderIframe("/" + file.path); } } @@ -229,53 +217,61 @@ async function renderMarkdown(rel) { els.viewer.innerHTML = `
${marked.parse(src || "# Untitled")}
`; } -function renderIframe(src) { - const iframe = document.createElement("iframe"); - iframe.src = src; - iframe.loading = "eager"; - iframe.setAttribute("sandbox", "allow-same-origin allow-scripts allow-forms allow-popups"); - iframe.style.width = "100vw"; - iframe.style.height = "calc(100vh - var(--topbar-h) - var(--subnav-h))"; - iframe.style.border = "none"; - iframe.style.borderRadius = "0"; - iframe.style.margin = "0"; +// === PREVIEW + PORTAL ENGINE === +async function renderIframe(rel) { + const preview = await generatePreview(rel); + const portalBtn = ``; - els.viewer.appendChild(iframe); + els.viewer.innerHTML = ` +
${portalBtn}
+
${preview}
+ `; - iframe.onload = () => { - try { - const doc = iframe.contentDocument; - const style = doc.createElement("style"); - style.textContent = ` - html, body { - background: transparent; - color: inherit; - font-family: inherit; - margin: 0; - padding: 3rem 4vw; - } - * { max-width: 100%; } - img, video, iframe { max-width: 100%; height: auto; } - `; - doc.head.appendChild(style); - - const body = doc.body; - const hasContent = body && body.innerText.trim().length > 50; - if (!hasContent) { - doc.body.innerHTML = ` -
-

${src.split("/").pop().replace(/\..*$/, "")}

-

No content yet.

-
- `; - doc.body.style.background = "transparent"; - } - } catch (e) {} - }; + els.viewer.querySelector(".portal-btn").addEventListener("click", e => { + window.open(e.target.dataset.src, "_blank", "noopener,noreferrer"); + }); } +async function generatePreview(rel) { + try { + const res = await fetch(rel); + if (!res.ok) throw new Error(); + const html = await res.text(); + + let content = html.match(/]*>([\s\S]*)<\/body>/i)?.[1] || html; + + // Strip dangerous/interactive elements + content = content + .replace(//gi, "") + .replace(//gi, "") + .replace(/]*rel=["']stylesheet["'][^>]*>/gi, "") + .replace(/\s+(on\w+)=["'][^"']*["']/gi, "") + .replace(/\s+style=["'][^"']*["']/gi, ""); + + const div = document.createElement("div"); + div.innerHTML = content; + trimPreview(div, 3, 3000); // depth, char limit + + return div.innerHTML || `

Empty content.

`; + } catch { + return `

Preview unavailable. Open directly.

`; + } +} + +function trimPreview(el, maxDepth, charLimit, depth = 0, chars = 0) { + if (depth > maxDepth || chars > charLimit) { + el.innerHTML = "..."; + return; + } + for (const child of [...el.children]) { + trimPreview(child, maxDepth, charLimit, depth + 1, chars + child.textContent.length); + if (chars > charLimit) child.remove(); + } +} + +// === DEFAULT VIEW === function renderDefault() { - const defaultSection = indexData.sections.includes("posts") ? "posts" : (indexData.sections[0] || null); + const defaultSection = indexData.sections.includes("posts") ? "posts" : indexData.sections[0]; if (defaultSection) { els.sectionSelect.value = defaultSection; renderList(); @@ -285,4 +281,5 @@ function renderDefault() { } } +// === START === init(); diff --git a/public/index.html b/public/index.html index bd87a71..af81a08 100755 --- a/public/index.html +++ b/public/index.html @@ -1,63 +1,62 @@ - - -The Fold Within - - - - - - + + + The Fold Within + + + + + + -
- - -
+
+ + +
- - + - -
-
-
+
+
    +
    + +
    + Filters & Search +
    + + + + + + + + + + + + + +
    +
    + + +
    +
    +
    - \ No newline at end of file + diff --git a/public/styles.css b/public/styles.css index f2486df..c60e900 100755 --- a/public/styles.css +++ b/public/styles.css @@ -17,6 +17,7 @@ html, body { line-height: 1.6; } +/* === TOPBAR & NAV === */ .topbar { position: fixed; top: 0; left: 0; right: 0; height: var(--topbar-h); display: flex; align-items: center; @@ -48,7 +49,7 @@ html, body { text-shadow: 0 0 8px var(--accent); } -/* NESTED HORIZON: Emergent Sub-Navigation with Animation */ +/* === SUBNAV === */ .sub-nav { display: none; position: fixed; @@ -92,6 +93,7 @@ html, body { text-shadow: 0 0 6px var(--accent); } +/* === SIDEBAR === */ #sidebar { position: fixed; top: calc(var(--topbar-h) + var(--subnav-h)); left: 0; width: 300px; bottom: 0; overflow: auto; @@ -180,7 +182,7 @@ body.sidebar-open #sidebar { transform: translateX(0); } .content { margin-left: 300px; } } -/* BREATHING HORIZON: Full-Field Viewer */ +/* === VIEWER – BREATHING FIELD === */ .viewer { display: flex; flex-direction: column; @@ -204,20 +206,19 @@ body.sidebar-open #sidebar { transform: translateX(0); } to { opacity: 1; transform: translateY(0); } } -/* HARMONIZER HEADER */ -.harmonizer-header { +/* === PREVIEW + PORTAL === */ +.preview-header { display: flex; justify-content: flex-end; - align-items: center; padding: 0.5rem 1rem; - background: rgba(255, 215, 0, 0.05); + background: rgba(224, 184, 75, 0.08); border-bottom: 1px solid #333; position: sticky; top: 0; z-index: 10; } -.popout-btn { +.portal-btn { background: none; border: 1px solid var(--accent); color: var(--accent); @@ -225,47 +226,46 @@ body.sidebar-open #sidebar { transform: translateX(0); } padding: 0.25rem 0.75rem; font-size: 0.85rem; cursor: pointer; - transition: background 0.2s ease, color 0.2s ease; + transition: all 0.2s ease; } -.popout-btn:hover { +.portal-btn:hover { background: var(--accent); color: var(--bg); } -/* HARMONIZED BODY CONTEXT */ -.harmonized { - background: transparent; - color: var(--fg); - font-family: 'Inter', system-ui, sans-serif; +.preview-content { + padding: 3rem 4vw; max-width: 90ch; margin: auto; - padding: 3rem 4vw; line-height: 1.7; + color: var(--fg); + font-family: 'Inter', system-ui, sans-serif; } -.harmonized h1, .harmonized h2, .harmonized h3, .harmonized h4 { - color: var(--accent); - border-bottom: 1px solid #333; - padding-bottom: 0.3em; - margin-top: 2em; +.preview-content * { + background: transparent !important; + color: inherit !important; + font-family: inherit !important; } -.harmonized img, .harmonized video, .harmonized iframe { - display: block; - margin: 2rem auto; +.preview-content img, +.preview-content video, +.preview-content iframe { max-width: 100%; height: auto; - border-radius: var(--radius); + border-radius: 8px; + margin: 1.5rem 0; + display: block; } -.harmonized a { +.preview-content a { color: var(--accent); text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 2px; } -.harmonized a:hover { +.preview-content a:hover { text-shadow: 0 0 6px var(--accent); } diff --git a/tools/generate-index.mjs b/tools/generate-index.mjs index a6b5948..9bd5a10 100755 --- a/tools/generate-index.mjs +++ b/tools/generate-index.mjs @@ -1,4 +1,10 @@ #!/usr/bin/env node +/** + * generate-index.mjs + * Scans public/ for .md, .html, .pdf + * Builds index.json: flat list, sections, tags, hierarchies + * Zero config. Filesystem = truth. + */ import { promises as fs } from "fs"; import path from "path"; import pdf from "pdf-parse"; @@ -57,7 +63,6 @@ async function collectFiles(relBase = "", flat = []) { const rel = path.posix.join(relBase, e.name); const absPath = path.join(ROOT, rel); - // Skip the SPA root index file entirely — it's the shell, not content if (rel.toLowerCase() === "index.html" || rel.toLowerCase() === "index.md") continue; if (e.isDirectory()) { @@ -104,24 +109,19 @@ async function collectFiles(relBase = "", flat = []) { (async () => { try { const flat = await collectFiles(); - - // Build sections: folders with non-index files const sections = [...new Set(flat.filter(f => !f.isIndex).map(f => f.path.split("/")[0]))].sort(); - - // Build hierarchies: parent → [child] where child has index.* const hierarchies = {}; for (const f of flat.filter(f => f.isIndex)) { const parts = f.path.split("/"); - if (parts.length > 2) { // e.g., essays/ai/index.md → parts[0]=essays, parts[1]=ai - const parent = parts[0]; - const child = parts[1]; + if (parts.length > 2) { + const parent = parts.slice(0, -2).join("/"); + const child = parts[parts.length - 2]; if (!hierarchies[parent]) hierarchies[parent] = []; if (!hierarchies[parent].includes(child)) { hierarchies[parent].push(child); } } } - const allTags = [...new Set(flat.flatMap(f => f.tags))].sort(); await fs.writeFile(OUT, JSON.stringify({ flat, sections, tags: allTags, hierarchies }, null, 2)); @@ -130,4 +130,4 @@ async function collectFiles(relBase = "", flat = []) { console.error("Build failed:", e); process.exit(1); } -})(); \ No newline at end of file +})();