feat(action-points): add zensical.toml kill switch + perf + legibility

Feature flag:
- [project.extra.action_points] enabled (default true) in zensical.toml.
- overrides/main.html exposes it as window.TOA_ACTION_POINTS_ENABLED in
  <head>, before extra_javascript loads.
- extra.js bails at the top of the IIFE when the flag is false: no
  storage, no nav badges, no heading pills, no confetti, no per-click
  work. Checkboxes fall back to Material's read-only tasklist.
  Verified inert when disabled (0 badges/pills/canvases, no localStorage,
  no JS errors).

Perf:
- In-memory write-through cache for the parsed store: a single toggle
  went from 15 localStorage JSON.parse reads to 0.
- updateNavBadges() updates badges in place (reuse span, only write on
  change) instead of removing+rebuilding every badge each toggle, which
  forced a full Material nav relayout. JS/toggle ~0.37->0.26ms,
  layout delta ~0.73->0.63ms.

Legibility:
- Override -webkit-text-fill-color (headings set it; pseudo-elements
  inherit it and it beats `color`) to #fff on completed green pills and
  nav badges, so the count/checkmark is white on green. h2 text color
  itself untouched.
This commit is contained in:
Tolaria 2026-06-03 23:46:14 +08:00
parent fbc488cc95
commit c7a3fe9412
4 changed files with 107 additions and 21 deletions

View file

@ -95,6 +95,13 @@
;(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;
@ -107,15 +114,30 @@
}
// ── 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) return _fresh();
if (!raw) {
_storeCache = _fresh();
return _storeCache;
}
var data = JSON.parse(raw);
if (data.version !== VERSION) return _migrate(data);
return data;
if (data.version !== VERSION) {
_storeCache = _migrate(data);
return _storeCache;
}
_storeCache = data;
return _storeCache;
} catch (e) {
return _fresh();
_storeCache = _fresh();
return _storeCache;
}
}
@ -148,6 +170,7 @@
}
function saveStorage(data) {
_storeCache = data; // keep the in-memory cache authoritative
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
} catch (e) {
@ -636,11 +659,6 @@
// Pause observer during our own DOM changes to prevent feedback loops
if (_navObserver) _navObserver.disconnect();
// Remove old badges
document.querySelectorAll(".toa-nav-badge").forEach(function (el) {
el.remove();
});
var store = getStorage();
// Only badge PAGE links in the primary navigation. Exclude:
// - the secondary nav ("On this page" / table-of-contents), whose links
@ -652,16 +670,29 @@
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) return;
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")) return;
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 += "/";
@ -689,19 +720,34 @@
badge = getPageSummary(url);
}
if (!badge || badge.total === 0) return;
if (!badge || badge.total === 0) {
if (existing) existing.remove();
link.classList.remove("toa-nav--complete");
return;
}
var span = document.createElement("span");
span.className = "toa-nav-badge";
if (badge.checked === badge.total) {
span.className += " toa-nav-badge--complete";
span.textContent = "\u2713"; // ✓
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 {
span.textContent = badge.checked + "/" + badge.total;
}
} else if (link.classList.contains("toa-nav--complete")) {
link.classList.remove("toa-nav--complete");
}
link.appendChild(span);
});
// Resume observer

View file

@ -646,6 +646,10 @@ 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;
}
@ -678,6 +682,7 @@ h3[data-toa-complete="true"]::after {
.toa-nav-badge--complete {
background: #3d6b52;
color: #fff !important;
-webkit-text-fill-color: #fff !important;
border-color: #3d6b52;
}
@ -686,6 +691,7 @@ h3[data-toa-complete="true"]::after {
!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 {

23
overrides/main.html Normal file
View file

@ -0,0 +1,23 @@
{% 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

@ -312,6 +312,17 @@ host = "https://cusdis.krystl.org"
app_id = "d9ab4b66-d5f1-4514-a475-1a824f69530f"
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]]
#icon = "fontawesome/brands/github"
#link = "https://github.com/user/repo"