From 7e42c3854d6ea2ac2acceac3aa449780840d055f Mon Sep 17 00:00:00 2001 From: forgejo-actions Date: Wed, 3 Jun 2026 15:54:37 +0000 Subject: [PATCH] deploy: zensical build from 4421378 --- 404.html | 6 + contributing/index.html | 6 + contributing/markdown/index.html | 6 + index.html | 6 + javascripts/extra.js | 796 ++++++++++++++++++++ javascripts/toa-totals.json | 4 + practices/daily-sadhana/index.html | 37 +- practices/digital-wellness/index.html | 6 + practices/grounding-in-nature/index.html | 6 + practices/index.html | 6 + practices/nourishing-the-body/index.html | 6 + practices/spiritual-hygiene/index.html | 6 + search.json | 2 +- strategies/collective-awakening/index.html | 6 + strategies/community-sovereignty/index.html | 6 + strategies/digital-sovereignty/index.html | 20 +- strategies/finding-purpose/index.html | 6 + strategies/index.html | 6 + stylesheets/extra.css | 119 +++ 19 files changed, 1044 insertions(+), 12 deletions(-) create mode 100644 javascripts/toa-totals.json diff --git a/404.html b/404.html index a54c8cb..d49a9aa 100644 --- a/404.html +++ b/404.html @@ -62,6 +62,12 @@ + + + diff --git a/contributing/index.html b/contributing/index.html index ddddbcb..a0019e3 100644 --- a/contributing/index.html +++ b/contributing/index.html @@ -68,6 +68,12 @@ + + + diff --git a/contributing/markdown/index.html b/contributing/markdown/index.html index e052837..11cf741 100644 --- a/contributing/markdown/index.html +++ b/contributing/markdown/index.html @@ -68,6 +68,12 @@ + + + diff --git a/index.html b/index.html index 269b621..0b76da1 100644 --- a/index.html +++ b/index.html @@ -66,6 +66,12 @@ + + + diff --git a/javascripts/extra.js b/javascripts/extra.js index d3adc8b..7064227 100644 --- a/javascripts/extra.js +++ b/javascripts/extra.js @@ -111,3 +111,799 @@ boot(); } })(); + +/* ═══════════════════════════════════════════════════════════════════════════ + Action Points — Interactive Checklist + ═══════════════════════════════════════════════════════════════════════ */ +;(function () { + "use strict"; + + // ── Feature flag ─────────────────────────────────── + // Disabled via zensical.toml: [project.extra.action_points] enabled = false + // (surfaced as window.TOA_ACTION_POINTS_ENABLED by overrides/main.html). + // When off, bail before ANY listeners, observers, storage, or per-click work + // are wired up — checkboxes fall back to Material's read-only rendering. + if (window.TOA_ACTION_POINTS_ENABLED === false) return; + + // ── Constants ────────────────────────────────────── + var STORAGE_KEY = "toa-action-points"; + var VERSION = 2; + var REDUCED_MOTION = window.matchMedia( + "(prefers-reduced-motion: reduce)" + ).matches; + + function todayStr() { + return new Date().toISOString().slice(0, 10); + } + + // ── Storage ──────────────────────────────────────── + // In-memory write-through cache of the parsed store. A single checkbox toggle + // calls getStorage() ~15x; without this each call re-parsed the whole JSON + // blob (and saveStorage re-stringified it 3x). The cache keeps reads O(1) and + // is kept in sync on every save, so reflow — not parsing — dominates a toggle. + var _storeCache = null; + + function getStorage() { + if (_storeCache) return _storeCache; + try { + var raw = localStorage.getItem(STORAGE_KEY); + if (!raw) { + _storeCache = _fresh(); + return _storeCache; + } + var data = JSON.parse(raw); + if (data.version !== VERSION) { + _storeCache = _migrate(data); + return _storeCache; + } + _storeCache = data; + return _storeCache; + } catch (e) { + _storeCache = _fresh(); + return _storeCache; + } + } + + function _fresh() { + return { version: VERSION, pages: {} }; + } + + function _migrate(old) { + // v1 → v2: add completedDates tracking + var data = _fresh(); + if (old.pages) { + Object.keys(old.pages).forEach(function (path) { + var op = old.pages[path]; + data.pages[path] = { + lastUpdated: op.lastUpdated || null, + checked: op.checked || [], + completedDates: {}, + _checked: op._checked, + _total: op._total, + }; + // Seed completedDates from checked array (assume checked=today) + var td = todayStr(); + (op.checked || []).forEach(function (label) { + data.pages[path].completedDates[label] = [td]; + }); + }); + } + saveStorage(data); + return data; + } + + function saveStorage(data) { + _storeCache = data; // keep the in-memory cache authoritative + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); + } catch (e) { + /* quota exceeded — silently drop */ + } + } + + function getPagePath() { + var p = location.pathname; + if (p.slice(-1) !== "/") p += "/"; + return p; + } + + function getNormLabel(li) { + // Clone li, remove the task-list-control label, get remaining text + var clone = li.cloneNode(true); + var ctrl = clone.querySelector(".task-list-control"); + if (ctrl) ctrl.remove(); + return (clone.textContent || "").replace(/\s+/g, " ").trim().toLowerCase(); + } + + function getPageData(path) { + var store = getStorage(); + if (!store.pages[path]) { + store.pages[path] = { checked: [], completedDates: {}, lastUpdated: null }; + } + if (!store.pages[path].completedDates) { + store.pages[path].completedDates = {}; + } + return store.pages[path]; + } + + function setPageChecked(path, label, checked) { + var store = getStorage(); + if (!store.pages[path]) { + store.pages[path] = { checked: [], completedDates: {}, lastUpdated: null }; + } + var page = store.pages[path]; + if (!page.completedDates) page.completedDates = {}; + if (!page.completedDates[label]) page.completedDates[label] = []; + + var td = todayStr(); + if (checked) { + if (page.checked.indexOf(label) === -1) page.checked.push(label); + if (page.completedDates[label].indexOf(td) === -1) { + page.completedDates[label].push(td); + } + } else { + page.checked = page.checked.filter(function (l) { + return l !== label; + }); + page.completedDates[label] = page.completedDates[label].filter(function (d) { + return d !== td; + }); + } + page.lastUpdated = new Date().toISOString(); + saveStorage(store); + } + + // Persist page summary (checked count, total) for nav aggregation + function savePageSummary(path, checkedCount, total) { + var store = getStorage(); + if (!store.pages[path]) { + store.pages[path] = { checked: [], lastUpdated: null }; + } + store.pages[path]._checked = checkedCount; + store.pages[path]._total = total; + // Don't bump lastUpdated for summary-only writes + saveStorage(store); + } + + function getPageSummary(path) { + var store = getStorage(); + var page = store.pages[path]; + if (!page || page._total === undefined) return null; + return { checked: page._checked || 0, total: page._total || 0 }; + } + + // ── Build-time totals manifest ───────────────────── + // toa-totals.json maps page-URL -> task count, generated at build time + // (see main.py). Seeding _total for every page lets nav badges show + // correct totals on FIRST load, before the user has visited each page. + // Fetched once per session; cached by the browser thereafter. + var _totalsLoaded = false; + + function seedTotalsFromManifest(cb) { + if (_totalsLoaded) { + cb && cb(false); + return; + } + _totalsLoaded = true; // mark immediately so concurrent navs don't double-fetch + + // Resolve the manifest relative to the site root. The script tag for + // extra.js is loaded from /javascripts/extra.js, so derive the URL + // from document scripts to stay correct under any base_url. + var url = "javascripts/toa-totals.json"; + try { + var scripts = document.querySelectorAll('script[src*="javascripts/extra.js"]'); + if (scripts.length) { + url = scripts[0].src.replace(/extra\.js.*$/, "toa-totals.json"); + } else { + // Fallback: resolve from origin + url = new URL("javascripts/toa-totals.json", location.origin + "/").href; + } + } catch (e) { + /* use relative fallback */ + } + + fetch(url, { cache: "force-cache" }) + .then(function (r) { + return r.ok ? r.json() : null; + }) + .then(function (totals) { + if (!totals) { + cb && cb(false); + return; + } + var store = getStorage(); + var changed = false; + Object.keys(totals).forEach(function (p) { + var path = p.slice(-1) === "/" ? p : p + "/"; + if (!store.pages[path]) { + store.pages[path] = { checked: [], completedDates: {}, lastUpdated: null }; + } + // Only seed _total; never clobber _checked (real progress wins). + if (store.pages[path]._total !== totals[p]) { + store.pages[path]._total = totals[p]; + changed = true; + } + if (store.pages[path]._checked === undefined) { + store.pages[path]._checked = (store.pages[path].checked || []).length; + } + }); + if (changed) saveStorage(store); + cb && cb(true); + }) + .catch(function () { + cb && cb(false); + }); + } + + // ── Checkbox initialization ──────────────────────── + function initCheckboxes() { + var path = getPagePath(); + var pageData = getPageData(path); + + var items = document.querySelectorAll("li.task-list-item"); + if (!items.length) return { checked: 0, total: 0 }; + + var total = items.length; + var checkedCount = 0; + + items.forEach(function (li) { + var cb = li.querySelector('input[type="checkbox"]'); + if (!cb) return; + + var label = getNormLabel(li); + var isChecked = pageData.checked.indexOf(label) !== -1; + + // Enable checkbox + cb.disabled = false; + cb.checked = isChecked; + if (isChecked) checkedCount++; + + cb.addEventListener("change", function () { + var now = cb.checked; + setPageChecked(path, label, now); + + // Recompute & re-render + var counts = computeHeadingCounts(); + savePageSummary(path, counts.pageChecked, counts.pageTotal); + renderHeadingPills(counts); + updatePageHeading(counts); + updateNavBadges(); + checkCompletions(counts); + }); + }); + + return { checked: checkedCount, total: total }; + } + + // ── Heading counters ─────────────────────────────── + function computeHeadingCounts() { + var article = document.querySelector("article"); + if (!article) return { groups: [], pageChecked: 0, pageTotal: 0 }; + + var groups = []; + var current = null; + + // Tree-walk: assign items to the nearest preceding h2/h3 in document order. + // This is immune to compareDocumentPosition quirks. + function walk(node) { + if (node.nodeType !== 1) return; // skip non-elements + var tag = node.tagName; + if (tag === "H2" || tag === "H3") { + current = node; + groups.push({ heading: node, items: [], level: parseInt(tag[1]) }); + } else if (tag === "LI" && node.classList.contains("task-list-item")) { + if (current) { + groups[groups.length - 1].items.push(node); + } + } + // Recurse into children + var children = node.childNodes; + for (var i = 0; i < children.length; i++) { + walk(children[i]); + } + } + + // Walk article's children (skip article itself to avoid h1) + var kids = article.childNodes; + for (var i = 0; i < kids.length; i++) { + walk(kids[i]); + } + + // Compute checked counts + var pageChecked = 0; + var pageTotal = 0; + groups.forEach(function (g) { + var checked = 0; + g.items.forEach(function (li) { + var cb = li.querySelector('input[type="checkbox"]'); + if (cb && cb.checked) checked++; + }); + g.checked = checked; + g.total = g.items.length; + pageChecked += checked; + pageTotal += g.items.length; + }); + + return { groups: groups, pageChecked: pageChecked, pageTotal: pageTotal }; + } + + function renderHeadingPills(counts) { + // Clear previous data attributes from all article headings + document.querySelectorAll("article h2, article h3").forEach(function (h) { + h.removeAttribute("data-toa-count"); + h.removeAttribute("data-toa-complete"); + h.classList.remove("toa-heading--complete"); + }); + + counts.groups.forEach(function (g) { + if (g.total === 0) return; + g.heading.setAttribute("data-toa-count", g.checked + "/" + g.total); + if (g.checked === g.total) { + g.heading.setAttribute("data-toa-complete", "true"); + g.heading.classList.add("toa-heading--complete"); + } + }); + } + + function updatePageHeading(counts) { + var h1 = document.querySelector("article h1"); + if (!h1) return; + h1.removeAttribute("data-toa-count"); + h1.removeAttribute("data-toa-complete"); + h1.classList.remove("toa-heading--complete"); + + if (counts.pageTotal === 0) return; + + h1.setAttribute("data-toa-count", counts.pageChecked + "/" + counts.pageTotal); + if (counts.pageChecked === counts.pageTotal) { + h1.setAttribute("data-toa-complete", "true"); + h1.classList.add("toa-heading--complete"); + } + } + + // ── Completion detection ─────────────────────────── + var _completedHeadings = {}; + var _pageWasComplete = false; + + function checkCompletions(counts) { + var path = getPagePath(); + + // Check heading completions + counts.groups.forEach(function (g) { + if (g.total === 0) return; + var id = g.heading.id || g.heading.textContent.replace(/\s+/g, "_"); + var justCompleted = g.checked === g.total && !_completedHeadings[id]; + _completedHeadings[id] = g.checked === g.total; + + if (justCompleted) { + fireHeadingComplete(); + } + }); + + // Check page completion + var pageComplete = + counts.pageTotal > 0 && counts.pageChecked === counts.pageTotal; + if (pageComplete && !_pageWasComplete) { + firePageComplete(); + } + _pageWasComplete = pageComplete; + + // Save summary for nav aggregation + savePageSummary(path, counts.pageChecked, counts.pageTotal); + } + + // ── Confetti ─────────────────────────────────────── + // Single shared canvas + one rAF loop. Multiple completions add particles + // to the same pool instead of stacking N fullscreen canvases (each of which + // would independently repaint the whole viewport at 60fps — the cause of + // click-induced slowdown). The loop fully stops when no particles remain, + // and the canvas is removed so there is no idle fullscreen layer. + var _confettiCanvas = null; + var _confettiCtx = null; + var _confettiParticles = []; + var _confettiRAF = 0; + + var CONFETTI_COLORS = [ + "#4a8262", "#a8c5b6", "#3d6b52", "#e9dfc6", "#92ad9e", "#d4e8dc", + ]; + + function _ensureConfettiCanvas() { + if (_confettiCanvas) return; + var canvas = document.createElement("canvas"); + canvas.style.cssText = + "position:fixed;top:0;left:0;width:100vw;height:100vh;pointer-events:none;z-index:99999;"; + document.body.appendChild(canvas); + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + _confettiCanvas = canvas; + _confettiCtx = canvas.getContext("2d"); + } + + function _destroyConfettiCanvas() { + if (_confettiCanvas) { + _confettiCanvas.remove(); + _confettiCanvas = null; + _confettiCtx = null; + } + _confettiParticles = []; + _confettiRAF = 0; + } + + function _confettiTick(now) { + var ctx = _confettiCtx; + var canvas = _confettiCanvas; + if (!ctx || !canvas) { + _confettiRAF = 0; + return; + } + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + var alive = 0; + for (var i = 0; i < _confettiParticles.length; i++) { + var p = _confettiParticles[i]; + var elapsed = now - p.start; + var progress = elapsed / p.duration; + if (progress >= 1) continue; // dead — skip + alive++; + + p.x += p.vx; + p.vy += 0.12; + p.y += p.vy; + p.rot += p.rotV; + var fade = progress > 0.7 ? 1 - (progress - 0.7) / 0.3 : 1; + + ctx.save(); + ctx.translate(p.x, p.y); + ctx.rotate((p.rot * Math.PI) / 180); + ctx.globalAlpha = fade; + ctx.fillStyle = p.color; + ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h); + ctx.restore(); + } + + if (alive === 0) { + _destroyConfettiCanvas(); + return; + } + + // Periodically compact the array so dead particles don't accumulate. + if (_confettiParticles.length > 400) { + _confettiParticles = _confettiParticles.filter(function (p) { + return now - p.start < p.duration; + }); + } + + _confettiRAF = requestAnimationFrame(_confettiTick); + } + + function fireConfetti(intensity) { + if (REDUCED_MOTION) return; + + _ensureConfettiCanvas(); + var canvas = _confettiCanvas; + + var count = intensity === "page" ? 140 : 55; + var duration = intensity === "page" ? 3200 : 2200; + var drift = intensity === "page" ? 1.8 : 1.2; + var start = performance.now(); + + for (var i = 0; i < count; i++) { + _confettiParticles.push({ + x: Math.random() * canvas.width, + y: -(Math.random() * 60 + 10), + w: Math.random() * 7 + 2, + h: Math.random() * 4 + 1.5, + color: CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)], + vx: (Math.random() - 0.5) * drift, + vy: Math.random() * 2 + 1.2, + rot: Math.random() * 360, + rotV: (Math.random() - 0.5) * 8, + start: start, + duration: duration, + }); + } + + // Start the single loop only if it isn't already running. + if (!_confettiRAF) { + _confettiRAF = requestAnimationFrame(_confettiTick); + } + } + + // Keep the shared canvas sized to the viewport. + window.addEventListener("resize", function () { + if (_confettiCanvas) { + _confettiCanvas.width = window.innerWidth; + _confettiCanvas.height = window.innerHeight; + } + }); + + // ── Sound ────────────────────────────────────────── + var _audioCtx = null; + + function getAudioCtx() { + if (!_audioCtx) { + _audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + } + if (_audioCtx.state === "suspended") { + _audioCtx.resume(); + } + return _audioCtx; + } + + function playTone(freq, duration, when) { + if (REDUCED_MOTION) return; + try { + var ctx = getAudioCtx(); + var osc = ctx.createOscillator(); + var gain = ctx.createGain(); + var t = when || ctx.currentTime; + + osc.type = "sine"; + osc.frequency.setValueAtTime(freq, t); + + gain.gain.setValueAtTime(0, t); + gain.gain.linearRampToValueAtTime(0.3, t + 0.04); + gain.gain.exponentialRampToValueAtTime(0.001, t + duration); + + osc.connect(gain); + gain.connect(ctx.destination); + osc.start(t); + osc.stop(t + duration); + } catch (e) { + /* Web Audio not available */ + } + } + + function fireHeadingComplete() { + playTone(432, 1.6); + fireConfetti("heading"); + } + + function firePageComplete() { + var ctx; + try { + ctx = getAudioCtx(); + } catch (e) { + fireConfetti("page"); + return; + } + // Ascending arpeggio centered on 528 Hz + var notes = [528 * 0.667, 528 * 0.75, 528 * 0.833, 528]; + var t = ctx.currentTime; + notes.forEach(function (freq, i) { + playTone(freq, 0.7, t + i * 0.18); + }); + fireConfetti("page"); + } + + // ── Navigation badges ────────────────────────────── + function updateNavBadges() { + // Pause observer during our own DOM changes to prevent feedback loops + if (_navObserver) _navObserver.disconnect(); + + var store = getStorage(); + // Only badge PAGE links in the primary navigation. Exclude: + // - the secondary nav ("On this page" / table-of-contents), whose links + // are in-page #anchors and would all resolve to the current page total + // - any link whose href is/contains a # fragment (same reason) + var navLinks = document.querySelectorAll( + ".md-nav--primary .md-nav__link[href]" + ); + + navLinks.forEach(function (link) { + var href = link.getAttribute("href"); + // Existing badge on this link, if any — we update in place rather than + // tearing down every badge in the sidebar each toggle (a full nav + // teardown/rebuild forces Material to relayout the whole sidebar, which + // was the dominant reflow cost per checkbox click). + var existing = link.querySelector(":scope > .toa-nav-badge"); + + // Skip in-page anchor links (TOC entries, heading permalinks). + if (!href || href.charAt(0) === "#" || href.indexOf("#") !== -1) { + if (existing) existing.remove(); + return; + } + // Defensive: skip if this link lives in a secondary (TOC) nav. + if (link.closest(".md-nav--secondary")) { + if (existing) existing.remove(); + return; + } + + // Resolve relative URLs to absolute paths + var url; + try { + url = new URL(href, location.origin + location.pathname).pathname; + } catch (e) { + if (existing) existing.remove(); + return; + } + if (url.slice(-1) !== "/") url += "/"; + + // Check if this is a section (index) page with child pages. + // NOTE: Material wraps the ACTIVE page's .md-nav__item with a nested + //