Compare commits

..

No commits in common. "4421378529a07bc97a6e8a8ba8a1fcb920aeecf3" and "5e0f482db14c95a85043e87d5eb55ea8d16a7419" have entirely different histories.

10 changed files with 13 additions and 1344 deletions

View file

@ -29,7 +29,6 @@ jobs:
python --version python --version
uv pip install zensical uv pip install zensical
uv pip install -e . uv pip install -e .
python3 main.py
zensical build --clean zensical build --clean
- name: Deploy to pages branch - name: Deploy to pages branch

View file

@ -111,799 +111,3 @@
boot(); 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 <base>/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
// <nav class="md-nav md-nav--secondary"> holding the "On this page" TOC.
// A naive ".md-nav--nested, nav.md-nav" probe matches that TOC and
// mis-classifies the active leaf page as a section. So we detect real
// section items via Material's own marker class and explicitly ignore
// any nested nav that is the secondary (TOC) nav.
var parentNav = link.closest(".md-nav__item");
var nestedNav = parentNav
? parentNav.querySelector(":scope > nav.md-nav:not(.md-nav--secondary)")
: null;
var isSection =
(parentNav && parentNav.classList.contains("md-nav__item--nested")) ||
!!nestedNav;
var badge;
if (isSection) {
// Aggregate all sub-pages whose paths start with this URL
badge = aggregateChildren(url, store);
} else {
badge = getPageSummary(url);
}
if (!badge || badge.total === 0) {
if (existing) existing.remove();
link.classList.remove("toa-nav--complete");
return;
}
var isComplete = badge.checked === badge.total;
var desiredText = isComplete ? "\u2713" : badge.checked + "/" + badge.total;
var desiredClass = isComplete
? "toa-nav-badge toa-nav-badge--complete"
: "toa-nav-badge";
// Reuse the existing span; only write when something actually changed, so
// unchanged links cause no DOM mutation and no style recalc.
var span = existing;
if (!span) {
span = document.createElement("span");
link.appendChild(span);
}
if (span.className !== desiredClass) span.className = desiredClass;
if (span.textContent !== desiredText) span.textContent = desiredText;
if (isComplete) {
if (!link.classList.contains("toa-nav--complete")) {
link.classList.add("toa-nav--complete");
}
} else if (link.classList.contains("toa-nav--complete")) {
link.classList.remove("toa-nav--complete");
}
});
// Resume observer
if (_navObserver) {
var sb = document.querySelector(".md-sidebar--primary");
if (sb) _navObserver.observe(sb, { childList: true, subtree: true });
}
}
function aggregateChildren(parentUrl, store) {
var checked = 0;
var total = 0;
Object.keys(store.pages).forEach(function (path) {
if (path === parentUrl) return;
if (path.indexOf(parentUrl) === 0) {
var s = store.pages[path];
if (s && s._total !== undefined) {
checked += s._checked || 0;
total += s._total || 0;
}
}
});
if (total === 0) return null;
return { checked: checked, total: total };
}
// ── Export ─────────────────────────────────────────
function createExportLink() {
// Only add if there's data to export
var store = getStorage();
var hasData = Object.keys(store.pages).length > 0;
var existing = document.querySelector(".toa-export");
if (existing) existing.remove();
if (!hasData) return;
// Find a good spot — footer copyright area or near comments
var container =
document.querySelector(".md-copyright") ||
document.querySelector("#__comments") ||
document.querySelector("article");
if (!container) return;
var link = document.createElement("a");
link.className = "toa-export";
link.textContent = "Download my progress";
link.href = "#";
link.addEventListener("click", function (e) {
e.preventDefault();
var raw = localStorage.getItem(STORAGE_KEY) || "{}";
var blob = new Blob([raw], { type: "application/json" });
var url = URL.createObjectURL(blob);
var a = document.createElement("a");
a.href = url;
a.download = "tree-of-ascension-progress.json";
a.click();
URL.revokeObjectURL(url);
});
container.appendChild(link);
}
// ── Main init ──────────────────────────────────────
var _lastInitPath = null;
function init() {
var path = getPagePath();
if (_lastInitPath === path) return; // guard against duplicate instant-nav fires
_lastInitPath = path;
_completedHeadings = {};
_pageWasComplete = false;
var result = initCheckboxes();
var counts = computeHeadingCounts();
savePageSummary(path, result.checked, result.total);
renderHeadingPills(counts);
updatePageHeading(counts);
updateNavBadges();
createExportLink();
// Seed build-time totals so nav badges show correct numbers for pages
// the user hasn't visited yet, then re-render badges once loaded.
seedTotalsFromManifest(function (ok) {
if (ok) updateNavBadges();
});
// Mark existing completions so we don't re-trigger on page load
counts.groups.forEach(function (g) {
if (g.total === 0) return;
var id = g.heading.id || g.heading.textContent.replace(/\s+/g, "_");
_completedHeadings[id] = g.checked === g.total;
if (g.checked === g.total) {
g.heading.classList.add("toa-heading--complete");
}
});
_pageWasComplete =
counts.pageTotal > 0 && counts.pageChecked === counts.pageTotal;
}
// ── Boot ───────────────────────────────────────────
var _navObserver = null;
function setupNavObserver() {
if (_navObserver) return;
var sidebar = document.querySelector(".md-sidebar--primary");
if (!sidebar) return;
_navObserver = new MutationObserver(function () {
updateNavBadges();
});
_navObserver.observe(sidebar, { childList: true, subtree: true });
}
function ready(fn) {
if (typeof document$ !== "undefined") {
document$.subscribe(function () {
requestAnimationFrame(function () {
requestAnimationFrame(fn);
});
setupNavObserver();
});
} else {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function () {
fn();
setupNavObserver();
});
} else {
fn();
setupNavObserver();
}
}
}
ready(init);
})();

View file

@ -1,4 +0,0 @@
{
"/practices/daily-sadhana/": 13,
"/strategies/digital-sovereignty/": 7
}

View file

@ -15,9 +15,7 @@ icon: material/meditation
## Meditation ## Meditation
- [ ] Sit quietly for 5 minutes — a chair, a floor, anywhere Sit. Breathe. That's the whole instruction.
- [ ] Notice when the mind wanders and gently return to breath
- [ ] Do this every morning for one week
Start with five minutes. The mind will wander — that's what minds do. The practice is not emptying the mind but noticing when it has wandered and gently returning. Every return is a rep. Like any muscle, attention strengthens with use. Start with five minutes. The mind will wander — that's what minds do. The practice is not emptying the mind but noticing when it has wandered and gently returning. Every return is a rep. Like any muscle, attention strengthens with use.
@ -27,10 +25,7 @@ No app needed. No special cushion. A chair, a floor, a patch of grass — anywhe
## Gratitude journal ## Gratitude journal
- [ ] Before bed or first thing, write down 3 specific things you're grateful for Before bed or first thing in the morning, write down three things you are genuinely grateful for. Not generic blessings — specific moments. The warmth of sunlight through a window. A conversation that landed. The taste of a ripe fruit.
- [ ] Do this every day for one week and notice the shift
Not generic blessings — specific moments. The warmth of sunlight through a window. A conversation that landed. The taste of a ripe fruit.
Gratitude is not toxic positivity. It is the deliberate practice of noticing what is already good, which rewires the brain away from its negativity bias over time. Gratitude is not toxic positivity. It is the deliberate practice of noticing what is already good, which rewires the brain away from its negativity bias over time.
@ -40,10 +35,10 @@ Gratitude is not toxic positivity. It is the deliberate practice of noticing wha
This is the inner work few want to do: asking honestly where you are still reactive. This is the inner work few want to do: asking honestly where you are still reactive.
- [ ] Ask yourself: Where am I still carrying anger? - Where am I still carrying anger?
- [ ] Ask yourself: What am I afraid of? - What am I afraid of?
- [ ] Ask yourself: How can I be more patient today? - How can I be more patient today?
- [ ] Replace one judgment with kindness today - Where could I replace judgment with kindness?
Don't fix it all at once. Pick one thread and follow it. The goal is not to become perfect — it is to become less defended, more available, more unconditionally loving. Not toward everyone else first — toward yourself. Don't fix it all at once. Pick one thread and follow it. The goal is not to become perfect — it is to become less defended, more available, more unconditionally loving. Not toward everyone else first — toward yourself.
@ -58,9 +53,6 @@ The body needs two kinds of movement every day:
**Energy practice** — yoga, tai chi, qigong, or simple stretching with breath awareness. This moves *prana* (life force) through the channels of the body. Even ten minutes shifts the nervous system from fight-or-flight to rest-and-digest. **Energy practice** — yoga, tai chi, qigong, or simple stretching with breath awareness. This moves *prana* (life force) through the channels of the body. Even ten minutes shifts the nervous system from fight-or-flight to rest-and-digest.
- [ ] Do 10 minutes of energy practice this morning (yoga, stretching, tai chi)
- [ ] Do 20 minutes of cardio or strength work today
**Cardio and strength** — walking briskly, running, bodyweight exercises, lifting. The body evolved to move against resistance. Without it, the mind stagnates too. **Cardio and strength** — walking briskly, running, bodyweight exercises, lifting. The body evolved to move against resistance. Without it, the mind stagnates too.
Pair them. Energy practice in the morning to open; strength in the afternoon to ground. Pair them. Energy practice in the morning to open; strength in the afternoon to ground.
@ -69,8 +61,7 @@ Pair them. Energy practice in the morning to open; strength in the afternoon to
## Visualization: morning and night ## Visualization: morning and night
- [ ] Spend 2 minutes each morning visualizing the future you want — see it as real *now* Every morning and every night before sleep, spend two minutes visualizing the future you want — for yourself, your family, your community, your 'tribe.' See it as if it is real *now*. Feel the gratitude as if it has already arrived.
- [ ] Spend 2 minutes before sleep visualizing the same — feel the gratitude as if it arrived
This is not magical thinking. It is the practice of orienting the subconscious toward what you are building, so that during the day, you recognize the doors when they appear. This is not magical thinking. It is the practice of orienting the subconscious toward what you are building, so that during the day, you recognize the doors when they appear.

View file

@ -27,10 +27,9 @@ For phones, the path is similar:
## Network sovereignty ## Network sovereignty
- [ ] Switch your main computer from WiFi to Ethernet **Switch from WiFi to Ethernet** for your main computer. It's faster, more stable, and eliminates constant radio-frequency exposure at close range.
- [ ] On your phone: switch 5G to 4G, use airplane mode when not needed
- [ ] Install and use a trusted VPN with a clear no-logging policy **On your phone**: switch 5G to 4G (lower frequency = less power = less exposure). Keep your phone on airplane mode when not actively needed. Keep it away from your body — not in a pocket, not against your head. Zero wearables (no smartwatch, no fitness tracker, no bluetooth earbuds).
- [ ] Keep your phone away from your body — not in a pocket, not against your head
**VPN**: A simple, trusted VPN prevents your internet provider from building a profile of every site you visit. Choose one with a clear no-logging policy and preferably based outside surveillance alliances. **VPN**: A simple, trusted VPN prevents your internet provider from building a profile of every site you visit. Choose one with a clear no-logging policy and preferably based outside surveillance alliances.
@ -55,9 +54,7 @@ The principle is simple: **self-hosted first, local-first, privacy-first**. If y
## Passwords and keys ## Passwords and keys
- [ ] Install KeePassXC and create your first password database Use [KeePassXC](https://keepassxc.org/) — a local, offline, open-source password manager. Unlike browser-based password managers or online services, your password database lives on your device. You control the file. Nobody can leak what they don't have.
- [ ] Generate unique passwords for every important service
- [ ] Get a hardware security key (like a YubiKey) where possible
Generate unique passwords for every service. Use a hardware security key (like a YubiKey) where possible. Treat your digital keys as carefully as the keys to your home. Generate unique passwords for every service. Use a hardware security key (like a YubiKey) where possible. Treat your digital keys as carefully as the keys to your home.

View file

@ -858,123 +858,4 @@ article h1, article h2, article h3 {
min-height: 150px; min-height: 150px;
padding: 1.1rem 1.1rem 3.2rem 1.1rem; padding: 1.1rem 1.1rem 3.2rem 1.1rem;
} }
/*
Action Points Interactive Checklist
*/
/* ── Checkbox interaction ─────────────────────────── */
li.task-list-item input[type="checkbox"] {
cursor: pointer;
transition: transform 0.18s var(--gp-ease, cubic-bezier(0.22, 0.61, 0.36, 1));
}
li.task-list-item input[type="checkbox"]:active {
transform: scale(0.82);
}
li.task-list-item:has(input:checked) {
text-decoration: line-through;
color: var(--gp-text-muted);
opacity: 0.72;
transition: opacity 0.3s ease, color 0.3s ease;
}
/* ── Pill badges (via ::after pseudo-element — clean textContent) ── */
h1[data-toa-count]::after,
h2[data-toa-count]::after,
h3[data-toa-count]::after {
content: attr(data-toa-count);
display: inline-block;
float: right;
background: var(--gp-card-bg, rgba(255,255,255,0.74));
border: 1px solid var(--gp-card-border, rgba(122,170,150,0.22));
border-radius: 999px;
padding: 0.08em 0.62em;
font-size: 0.62em;
font-weight: 550;
color: var(--gp-text-muted, #4e5e4b);
margin-left: 0.5em;
line-height: 1.6;
letter-spacing: 0.01em;
transition:
background 0.35s ease,
color 0.35s ease,
border-color 0.35s ease;
}
h1[data-toa-count]::after {
font-size: 0.48em;
padding: 0.1em 0.7em;
}
h1[data-toa-complete="true"]::after,
h2[data-toa-complete="true"]::after,
h3[data-toa-complete="true"]::after {
background: #3d6b52;
color: #fff !important;
/* Headings set -webkit-text-fill-color (gradient/fill effect); pseudo-els
inherit it and it overrides `color`. Override it on the pill ONLY so the
count text is white on green the heading's own text color is untouched. */
-webkit-text-fill-color: #fff !important;
border-color: #3d6b52;
}
/* ── Heading completion ───────────────────────────── */
.toa-heading--complete {
color: #3d6b52 !important;
transition: color 0.5s ease;
}
/* ── Nav badges ───────────────────────────────────── */
.toa-nav-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2em;
background: var(--gp-card-bg, rgba(255,255,255,0.74));
border: 1px solid var(--gp-card-border, rgba(122,170,150,0.22));
border-radius: 999px;
padding: 0 0.42em;
font-size: 0.68em;
font-weight: 550;
color: var(--gp-text-muted, #4e5e4b);
margin-left: 0.4em;
line-height: 1.65;
transition:
background 0.35s ease,
color 0.35s ease;
}
.toa-nav-badge--complete {
background: #3d6b52;
color: #fff !important;
-webkit-text-fill-color: #fff !important;
border-color: #3d6b52;
}
/* The complete nav link goes green (below). Make sure the badge text on its
green background stays white and isn't dragged green by the link's
!important color. */
.md-nav__link.toa-nav--complete .toa-nav-badge--complete {
color: #fff !important;
-webkit-text-fill-color: #fff !important;
}
.md-nav__link.toa-nav--complete {
color: #3d6b52 !important;
transition: color 0.35s ease;
}
/* ── Export link ──────────────────────────────────── */
.toa-export {
display: inline-block;
font-size: 0.78rem;
color: var(--gp-text-muted, #4e5e4b);
cursor: pointer;
text-decoration: underline;
text-underline-offset: 3px;
text-decoration-color: var(--gp-secondary, #a8c5b6);
margin-top: 0.6rem;
transition: color 0.22s ease;
}
.toa-export:hover {
color: var(--gp-primary, #4a8262);
} }

73
main.py
View file

@ -1,74 +1,5 @@
#!/usr/bin/env python3 def main():
"""Generate the Tree of Ascension action-points totals manifest. print("Hello from up!")
Scans docs/**/*.md for Markdown tasklist items (`- [ ]` / `- [x]`), counts
them per page, and writes docs/javascripts/toa-totals.json mapping each
page's site URL -> total task count.
The action-points JS (docs/javascripts/extra.js) fetches this manifest once
on first load so nav badges show correct totals BEFORE a user has visited
every page. Counts are fully static (known from the Markdown), so this has
zero runtime cost it's a build-time artifact.
Run before `zensical build`:
python3 main.py
"""
from __future__ import annotations
import json
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parent
DOCS = ROOT / "docs"
OUT = DOCS / "javascripts" / "toa-totals.json"
# Matches a Markdown tasklist item: optional indent, list marker, [ ] or [x].
TASK_RE = re.compile(r"^\s*[-*+]\s+\[[ xX]\]\s+", re.MULTILINE)
# Matches fenced code blocks (``` or ~~~), incl. language hint, non-greedy.
# These contain tasklist *examples* (e.g. the Markdown reference doc) that render
# as plain text, NOT interactive checkboxes — so we strip them before counting.
FENCE_RE = re.compile(r"^([ \t]*)(`{3,}|~{3,}).*?\n.*?^\1\2[ \t]*$", re.MULTILINE | re.DOTALL)
def count_tasks(text: str) -> int:
"""Count real (interactive) tasklist items, excluding fenced code examples."""
stripped = FENCE_RE.sub("", text)
return len(TASK_RE.findall(stripped))
def url_for(md_path: Path) -> str:
"""Map a docs/*.md path to its built site URL (directory-style URLs).
docs/index.md -> /
docs/practices/index.md -> /practices/
docs/practices/daily-sadhana.md-> /practices/daily-sadhana/
"""
rel = md_path.relative_to(DOCS).with_suffix("")
parts = list(rel.parts)
if parts and parts[-1] == "index":
parts = parts[:-1]
path = "/" + "/".join(parts)
if not path.endswith("/"):
path += "/"
return path
def main() -> None:
totals: dict[str, int] = {}
for md in sorted(DOCS.rglob("*.md")):
text = md.read_text(encoding="utf-8")
count = count_tasks(text)
if count:
totals[url_for(md)] = count
OUT.parent.mkdir(parents=True, exist_ok=True)
OUT.write_text(json.dumps(totals, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print(f"toa-totals: wrote {len(totals)} page(s) -> {OUT.relative_to(ROOT)}")
for url, n in sorted(totals.items()):
print(f" {url} {n}")
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -1,23 +0,0 @@
{% extends "base.html" %}
{#
Action Points feature flag.
Exposes the zensical.toml setting `[project.extra.action_points] enabled`
to client-side JS as `window.TOA_ACTION_POINTS_ENABLED`. This block renders
in <head>, before extra_javascript loads in {% block scripts %}, so the
action-points engine in docs/javascripts/extra.js can bail out immediately
when the feature is disabled.
Set `enabled = false` in zensical.toml to turn the ENTIRE interactive
checklist feature off — no localStorage, no nav badges, no confetti, no
per-click work. Checkboxes then fall back to Material's default read-only
rendering. Defaults to ON when the key is absent.
#}
{% block extrahead %}
{{ super() }}
<script>
window.TOA_ACTION_POINTS_ENABLED =
{{ "false" if config.extra.action_points and config.extra.action_points.enabled == false else "true" }};
</script>
{% endblock %}

View file

@ -1,296 +0,0 @@
# Action Points — Interactive Checklist PRD
**Status:** Draft
**Author:** aja + Hermes
**Date:** 2026-06-03
---
## 1. Problem Statement
The Tree of Ascension site contains practices and strategies that invite the reader to *do* something — meditate, journal, take a walk barefoot, migrate off a service. Right now those calls to action are flat text. A reader who returns tomorrow has no way to see what they've already tried or what remains.
We want to make the site *participatory*: let readers check off action items, see their progress, and feel a sense of momentum. Completion should be visible and celebratory — not gamified in a manipulative way, but genuinely satisfying.
---
## 2. Feature Overview
| # | Feature | Summary |
|---|---------|---------|
| 1 | **Checkable action points** | Markdown `- [ ]` task lists that are clickable and persist checked state in `localStorage` |
| 2 | **Heading counters** | Each heading with action items underneath shows a counter (e.g., `3/5`). When all items are checked, the heading changes color and confetti drops from the top of the page |
| 3 | **Navigation badges** | Sidebar navigation items show a checkmark and color change when all action items on that page are checked |
| 4 | **Page-level completion** | The top-of-page heading (h1) reflects overall page completion with a counter |
---
## 3. Detailed Specifications
### 3.1 Checkable Action Points
**What they are:**
Any `- [ ]` Markdown task list item rendered on a page. The site already uses `pymdownx.tasklist` with `custom_checkbox = true`, which renders checkboxes. We layer interactivity + persistence on top.
**Behavior:**
- Clicking a checkbox toggles its state (unchecked ↔ checked)
- State is immediately persisted to `localStorage`
- On page load (and on instant navigation), previously saved state is restored
- If a checkbox's label text no longer matches any saved entry (content was edited), the stale entry is silently ignored
**Checkbox identification:**
Each checkbox is identified by a key derived from:
1. The page's canonical URL path (e.g., `/practices/daily-sadhana/`)
2. The normalized label text (trimmed, lowercased, collapsed whitespace)
```
Key = URL + "::" + normalized_label
```
This is stable across reloads and survives minor whitespace changes but intentionally breaks on substantial rewrites (which should reset progress anyway).
**Visual style:**
- Unchecked: current Zensical tasklist styling (green-tinted)
- Checked: strikethrough + muted color, consistent with existing site palette
---
### 3.2 Heading Counters & Completion Celebration
**Counter display:**
For each heading (h2, h3, h4) that has one or more action items in its scope, a pill badge is floated to the right of the heading:
```
## Meditation [0/2]
```
When partially complete:
```
## Meditation [1/2]
```
When fully complete — the heading and badge both shift to the site's accent aurora purple (`--gp-accent`).
**Heading scope:**
An action item belongs to the nearest preceding heading at or above its level. Concretely:
- Items after an h2 but before the next h2 belong to that h2
- Items after an h3 but before the next h2 or h3 belong to that h3
**Confetti trigger:**
When the last unchecked item under a heading is checked (counter goes from `(n-1)/n` to `n/n`):
- A confetti burst animates from the top of the viewport
- Confetti uses the site's palette (greens, aurora purple, soft gold)
- Animation lasts ~3 seconds
- Respects `prefers-reduced-motion` (skips entirely)
**Confetti implementation:** Custom canvas animation (zero dependency).
Two tiers:
- **Heading completion:** Subtle aurora particle shower — ~2s, calm, particles in site palette. Accompanied by a soft 432 Hz sine chime (Web Audio API, gentle attack/decay envelope).
- **Page-level completion:** More energetic burst — ~3s, denser particles. Accompanied by an ascending arpeggio centered on 528 Hz (Web Audio API, sine waves with smooth envelope).
Both respect `prefers-reduced-motion` (confetti skipped, sounds skipped).
---
### 3.3 Navigation Integration
**Sidebar nav items:**
Each link in the left sidebar that points to a page with action items:
- Displays a pill badge next to the link text showing the completion count (e.g., `2/5`)
- When all items on that page are complete, the badge becomes a ✓ checkmark and the link text shifts to the accent color
**Index page aggregation:**
Section index pages (e.g., `/practices/`) aggregate completion across all their sub-pages. The badge shows the combined total — e.g., `12/28` summing all practice pages. Sub-page grouping is derived from URL structure: `/practices/daily-sadhana/` is a child of `/practices/`.
**How it works:**
1. After a page renders and checkboxes are initialized, compute `{ checked, total }` for that page
2. Store the summary in `localStorage` under the page URL
3. Find the corresponding `<a>` element in the sidebar navigation and inject the badge/checkmark
4. Re-run on every page navigation (instant or full)
**Edge cases:**
- Pages with no action items: no badge, no color change
- Deleted pages: stale localStorage entries don't render (no nav element to match)
---
### 3.4 Page-Level Heading
The top-of-page `<h1>` gets a pill badge (right-aligned) showing overall page progress:
```
# Daily Sadhana [4/12]
```
Same color-change logic as section headings: when complete, the h1 turns accent-colored.
The h1 counter aggregates *all* action items on the page — it's the sum of all section counts.
---
### 3.5 Progress Export (trivial add-on)
A small "Download my progress" link in the page footer or near the comments section. Clicking it triggers a browser download of a JSON file containing the full localStorage state. This lets users:
- Back up their progress before clearing browser data
- Share their completion state (import could come later)
Implementation is ~10 lines: read `localStorage`, create a `Blob`, trigger download via a temporary `<a>` element.
---
## 4. Technical Design
### 4.1 localStorage Schema
```json
{
"toa-action-points": {
"version": 1,
"pages": {
"/practices/daily-sadhana/": {
"lastUpdated": "2026-06-03T12:00:00.000Z",
"checked": [
"sit. breathe. that's the whole instruction.",
"write down three things you are genuinely grateful for"
]
},
"/strategies/digital-sovereignty/": {
"lastUpdated": "2026-06-03T12:30:00.000Z",
"checked": [
"switch from wifi to ethernet for your main computer"
]
}
}
}
}
```
- Keys are the site-relative URL path (without domain; works across environments)
- `checked` is an array of normalized label strings
- `version` allows future schema migrations
- One key per page keeps reads/writes small
### 4.2 Instant Navigation Compatibility
The site uses `navigation.instant` (SPA-style page transitions). Zensical/Material for MkDocs exposes `document$` as an observable for navigation events.
Our JS hooks into:
- `DOMContentLoaded` — for initial page load
- `document$.subscribe` — for instant navigation transitions
On each navigation:
1. Wait for DOM to settle (the new page content is rendered)
2. Scan for tasklist items
3. Restore saved state from localStorage
4. Inject counters into headings and h1
5. Update sidebar nav badges for *all* pages (not just current — because the current page's completion may have changed)
### 4.3 Storage Limit
`localStorage` is typically 510 MB per origin. Our schema is compact (a few KB even with many pages). No practical risk of hitting the limit.
### 4.4 Performance
- DOM scanning on each navigation: O(n) where n = number of tasklist items on a page (typically < 30)
- Nav badge updates: O(sidebar links × pages with data), well under 1ms
- confetti: canvas-based, runs off main thread via `requestAnimationFrame`
All work is < 5ms per navigation; no perceptible impact.
---
## 5. Future: Cross-Device Persistence (v2)
### 5.1 Problem
`localStorage` is per-browser, per-device. A reader who checks items on their phone won't see them on their laptop.
### 5.2 Solution sketch
- User optionally enters their email address
- We store a mapping: `email → checked_items` on a lightweight server
- On page load, if an email is stored in localStorage, fetch state from the server
- On checkbox toggle, sync to server (debounced)
- No authentication required — email is the identifier (like Cusdis comments)
- Server can be a simple SQLite-backed Python/FastAPI service or even a Cloudflare Worker + D1
### 5.3 Why not now
- Requires deploying and maintaining a server
- Adds latency and failure modes
- `localStorage` solves 80% of the value with 0% of the infrastructure cost
- We ship v1, learn from usage, then decide if cross-device is worth it
---
## 6. Content Authoring Guide
For content authors, "action points" are simply `- [ ]` Markdown task lists. No special syntax, no frontmatter, no directives.
```markdown
## Meditation
- [ ] Sit quietly for 5 minutes
- [ ] Notice when the mind wanders and return to breath
- [ ] Do this every morning for one week
```
The JS layer automatically detects these and adds interactivity. To opt a page *out* of action-point tracking entirely (e.g., index pages with task lists that aren't meant to be checked off), add frontmatter:
```yaml
---
action_points: false
---
```
---
## 7. Open Questions
1. **Confetti style — subtle or playful?**
A gentle aurora-colored particle shower that respects the site's calm aesthetic, or a more energetic celebration? The site's ethos ("not gamified in a manipulative way, but genuinely satisfying") points toward subtle — soft particles in the site palette, ~2-second duration, no sound.
2. **Should index pages (e.g., `/practices/`) track action items?**
Index pages use grid cards, not task lists. Likely `action_points: false` by default for any page without `- [ ]` items. But if someone adds checklist items to an index, should they track? Probably yes — detection is automatic unless explicitly opted out.
3. **Counter placement on headings — inline badge or superscript?**
`## Meditation [0/2]` (inline, after heading text) vs `## Meditation` with a small pill badge. Inline is simpler, less DOM manipulation, and works with existing heading anchors. I'd recommend inline.
4. **Navigation badges — all pages or only pages with action items?**
Only pages with at least one action item should get a badge. Empty counters clutter the sidebar. This means we compute on first page visit and store the `total` count — pages with total=0 get no badge.
5. **When a user edits the Markdown source (adding/removing items), how should we handle stale localStorage entries?**
Stale entries (label text no longer matches any rendered checkbox) are silently dropped. New items start unchecked. This is the least-surprising behavior — edits "reset" progress only for changed items, not the whole page.
6. **Should completion state be exportable/downloadable?**
A "download my progress" button would let users save their state as a JSON file before clearing browser data. Nice-to-have for v1.1.
7. **Confetti: custom canvas or `canvas-confetti` library?**
- Custom: ~60 lines of JS, full control over palette and behavior, zero dependency, but more code to maintain
- `canvas-confetti`: battle-tested, good defaults, one function call, but adds ~7 KB dependency
Leaning custom — the palette matching and reduced-motion respect are trivial to implement and the site already values self-contained minimalism.
---
## 8. Implementation Plan (high-level)
| Phase | What | Files touched |
|-------|------|--------------|
| 1 | Core checkbox persistence (read/write localStorage) | `docs/javascripts/extra.js` |
| 2 | Heading counters + color change | `docs/javascripts/extra.js`, `docs/stylesheets/extra.css` |
| 3 | Confetti on completion | `docs/javascripts/extra.js` (custom canvas) |
| 4 | Navigation badge injection | `docs/javascripts/extra.js`, `docs/stylesheets/extra.css` |
| 5 | Page-level h1 counter | `docs/javascripts/extra.js`, `docs/stylesheets/extra.css` |
| 6 | Opt-out frontmatter support | `docs/javascripts/extra.js` |
| 7 | Test across all content pages + mobile | manual QA |
All JS goes in the existing `docs/javascripts/extra.js`. All CSS in `docs/stylesheets/extra.css`. No new files, no new dependencies, no build pipeline changes.
---
## 9. Success Criteria
- [ ] Clicking a `- [ ]` checkbox toggles its visual state
- [ ] Refreshing the page preserves checked state
- [ ] Navigating to another page and back preserves checked state
- [ ] Headings show accurate counters (e.g., `2/5`)
- [ ] When a heading reaches full completion, it changes to accent color
- [ ] Confetti plays when the last item under a heading is checked (and not on reduced-motion)
- [ ] Navigation sidebar shows badges for pages with action items
- [ ] Fully-completed pages show ✓ checkmark in nav
- [ ] Page h1 shows overall completion counter
- [ ] Zero console errors on any page
- [ ] Works on mobile (touch events fire correctly on checkboxes)
- [ ] No visual regression on pages without action items

View file

@ -312,17 +312,6 @@ host = "https://cusdis.krystl.org"
app_id = "d9ab4b66-d5f1-4514-a475-1a824f69530f" app_id = "d9ab4b66-d5f1-4514-a475-1a824f69530f"
lang = "en" lang = "en"
# ----------------------------------------------------------------------------
# Action Points — interactive checklist feature flag.
# Set enabled = false to disable the ENTIRE feature (localStorage progress,
# nav badges, heading pills, confetti, and all per-click JS). Checkboxes then
# render as Material's default read-only tasklist. The flag is exposed to the
# client via overrides/main.html as window.TOA_ACTION_POINTS_ENABLED.
# ----------------------------------------------------------------------------
[project.extra.action_points]
enabled = true
#[[project.extra.social]] #[[project.extra.social]]
#icon = "fontawesome/brands/github" #icon = "fontawesome/brands/github"
#link = "https://github.com/user/repo" #link = "https://github.com/user/repo"