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:
parent
fbc488cc95
commit
c7a3fe9412
4 changed files with 107 additions and 21 deletions
|
|
@ -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"; // ✓
|
||||
link.classList.add("toa-nav--complete");
|
||||
} else {
|
||||
span.textContent = badge.checked + "/" + badge.total;
|
||||
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");
|
||||
}
|
||||
link.appendChild(span);
|
||||
});
|
||||
|
||||
// Resume observer
|
||||
|
|
|
|||
|
|
@ -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
23
overrides/main.html
Normal 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 %}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue