filters + epistemic popups

This commit is contained in:
Levi Neuwirth 2026-04-12 10:40:58 -04:00
parent 56d9262585
commit e25a311dd9
12 changed files with 1063 additions and 59 deletions

View File

@ -4,11 +4,13 @@ module Site (rules) where
import Control.Monad (filterM, when) import Control.Monad (filterM, when)
import Data.List (isPrefixOf) import Data.List (isPrefixOf)
import Data.Maybe (fromMaybe) import Data.Maybe (catMaybes, fromMaybe)
import System.Environment (lookupEnv) import System.Environment (lookupEnv)
import System.FilePath (takeDirectory, takeFileName, replaceExtension) import System.FilePath (takeDirectory, takeFileName, replaceExtension)
import Text.Read (readMaybe)
import qualified Data.Aeson as Aeson import qualified Data.Aeson as Aeson
import qualified Data.ByteString.Lazy.Char8 as LBS import qualified Data.ByteString.Lazy.Char8 as LBS
import qualified Data.Map.Strict as Map
import Hakyll import Hakyll
import Authors (buildAllAuthors, applyAuthorRules) import Authors (buildAllAuthors, applyAuthorRules)
import Backlinks (backlinkRules) import Backlinks (backlinkRules)
@ -445,6 +447,24 @@ rules = do
let urls = [ "/" ++ r | Just r <- routes ] let urls = [ "/" ++ r | Just r <- routes ]
makeItem $ LBS.unpack (Aeson.encode urls) makeItem $ LBS.unpack (Aeson.encode urls)
-- ---------------------------------------------------------------------------
-- Epistemic metadata manifest — maps page URLs to epistemic fields
-- (status, confidence, importance, evidence, scope, novelty, practicality,
-- stability, score) for client-side search filtering.
-- ---------------------------------------------------------------------------
create ["data/epistemic-meta.json"] $ do
route idRoute
compile $ do
essays <- loadAll (allEssays .&&. hasNoVersion) :: Compiler [Item String]
posts <- loadAll ("content/blog/*.md" .&&. hasNoVersion) :: Compiler [Item String]
fiction <- loadAll ("content/fiction/*.md" .&&. hasNoVersion) :: Compiler [Item String]
poetry <- loadAll (allPoetry .&&. hasNoVersion) :: Compiler [Item String]
music <- loadAll ("content/music/*/index.md" .&&. hasNoVersion) :: Compiler [Item String]
let items = essays ++ posts ++ fiction ++ poetry ++ music
pairs <- mapM epistemicEntry items
let metaMap = Map.fromList (catMaybes pairs)
makeItem $ LBS.unpack (Aeson.encode metaMap)
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
-- Atom feed — all content sorted by date -- Atom feed — all content sorted by date
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
@ -485,3 +505,48 @@ rules = do
<> bodyField "description" <> bodyField "description"
<> defaultContext <> defaultContext
renderAtom musicFeedConfig feedCtx compositions renderAtom musicFeedConfig feedCtx compositions
-- ---------------------------------------------------------------------------
-- Epistemic metadata extraction
-- ---------------------------------------------------------------------------
-- | Extract epistemic metadata from a content item's frontmatter.
-- Returns Nothing if the item has no route or no epistemic fields.
epistemicEntry :: Item String -> Compiler (Maybe (String, Map.Map String String))
epistemicEntry item = do
let ident = itemIdentifier item
mRoute <- getRoute ident
case mRoute of
Nothing -> return Nothing
Just r -> do
meta <- getMetadata ident
let url = "/" ++ r
fields = catMaybes
[ grab "status" meta
, grab "confidence" meta
, grab "importance" meta
, grab "evidence" meta
, grab "scope" meta
, grab "novelty" meta
, grab "practicality" meta
, grab "stability" meta
]
obj = Map.fromList fields
-- Compute overall-score the same way Contexts.overallScoreField does.
obj' = case ( readMaybe =<< lookupString "confidence" meta :: Maybe Int
, readMaybe =<< lookupString "evidence" meta :: Maybe Int
) of
(Just conf, Just ev) ->
let raw :: Double
raw = fromIntegral conf / 100.0 * 0.6
+ fromIntegral (ev - 1) / 4.0 * 0.4
score = max 0 (min 100 (round (raw * 100.0) :: Int))
in Map.insert "score" (show score) obj
_ -> obj
if Map.null obj'
then return Nothing
else return (Just (url, obj'))
where
grab name meta = case lookupString name meta of
Just v -> Just (name, v)
Nothing -> Nothing

View File

@ -8,6 +8,105 @@ search: true
<button class="search-tab" data-tab="semantic" role="tab" aria-selected="false">Semantic</button> <button class="search-tab" data-tab="semantic" role="tab" aria-selected="false">Semantic</button>
</div> </div>
<div class="search-filter-controls">
<button class="library-filter-toggle" aria-expanded="false" aria-controls="search-filters">
Filters<span class="filter-toggle-badge"></span>
</button>
</div>
<div id="search-filters" class="library-filters" hidden>
<div class="filter-row">
<span class="filter-label" data-ep-term="status">status</span>
<div class="filter-options">
<button class="filter-btn filter-status-btn" data-value="draft">draft</button>
<button class="filter-btn filter-status-btn" data-value="working model">working model</button>
<button class="filter-btn filter-status-btn" data-value="durable">durable</button>
<button class="filter-btn filter-status-btn" data-value="refined">refined</button>
<button class="filter-btn filter-status-btn" data-value="superseded">superseded</button>
<button class="filter-btn filter-status-btn" data-value="deprecated">deprecated</button>
</div>
</div>
<div class="filter-row">
<span class="filter-label" data-ep-term="confidence">confidence</span>
<div class="filter-options">
<span class="filter-prefix">&ge;</span>
<input type="number" id="filter-confidence" class="filter-number" min="0" max="100" placeholder="&mdash;" aria-label="Minimum confidence" />
</div>
</div>
<div class="filter-row">
<span class="filter-label" data-ep-term="importance">importance</span>
<div class="filter-options">
<span class="filter-prefix">&ge;</span>
<button class="filter-btn filter-threshold-btn" data-field="importance" data-value="1">1</button>
<button class="filter-btn filter-threshold-btn" data-field="importance" data-value="2">2</button>
<button class="filter-btn filter-threshold-btn" data-field="importance" data-value="3">3</button>
<button class="filter-btn filter-threshold-btn" data-field="importance" data-value="4">4</button>
<button class="filter-btn filter-threshold-btn" data-field="importance" data-value="5">5</button>
</div>
</div>
<div class="filter-row">
<span class="filter-label" data-ep-term="evidence">evidence</span>
<div class="filter-options">
<span class="filter-prefix">&ge;</span>
<button class="filter-btn filter-threshold-btn" data-field="evidence" data-value="1">1</button>
<button class="filter-btn filter-threshold-btn" data-field="evidence" data-value="2">2</button>
<button class="filter-btn filter-threshold-btn" data-field="evidence" data-value="3">3</button>
<button class="filter-btn filter-threshold-btn" data-field="evidence" data-value="4">4</button>
<button class="filter-btn filter-threshold-btn" data-field="evidence" data-value="5">5</button>
</div>
</div>
<div class="filter-row">
<span class="filter-label" data-ep-term="trust">trust</span>
<div class="filter-options">
<span class="filter-prefix">&ge;</span>
<input type="number" id="filter-score" class="filter-number" min="0" max="100" placeholder="&mdash;" aria-label="Minimum trust score" />
</div>
</div>
<div class="filter-row">
<span class="filter-label" data-ep-term="scope">scope</span>
<div class="filter-options">
<span class="filter-prefix">&ge;</span>
<button class="filter-btn filter-ordinal-btn" data-field="scope" data-index="0">personal</button>
<button class="filter-btn filter-ordinal-btn" data-field="scope" data-index="1">average</button>
<button class="filter-btn filter-ordinal-btn" data-field="scope" data-index="2">broad</button>
<button class="filter-btn filter-ordinal-btn" data-field="scope" data-index="3">civilizational</button>
</div>
</div>
<div class="filter-row">
<span class="filter-label" data-ep-term="novelty">novelty</span>
<div class="filter-options">
<span class="filter-prefix">&ge;</span>
<button class="filter-btn filter-ordinal-btn" data-field="novelty" data-index="0">conventional</button>
<button class="filter-btn filter-ordinal-btn" data-field="novelty" data-index="1">moderate</button>
<button class="filter-btn filter-ordinal-btn" data-field="novelty" data-index="2">idiosyncratic</button>
<button class="filter-btn filter-ordinal-btn" data-field="novelty" data-index="3">innovative</button>
</div>
</div>
<div class="filter-row">
<span class="filter-label" data-ep-term="practicality">practicality</span>
<div class="filter-options">
<span class="filter-prefix">&ge;</span>
<button class="filter-btn filter-ordinal-btn" data-field="practicality" data-index="0">abstract</button>
<button class="filter-btn filter-ordinal-btn" data-field="practicality" data-index="1">moderate</button>
<button class="filter-btn filter-ordinal-btn" data-field="practicality" data-index="2">high</button>
<button class="filter-btn filter-ordinal-btn" data-field="practicality" data-index="3">exceptional</button>
</div>
</div>
<div class="filter-row">
<span class="filter-label" data-ep-term="stability">stability</span>
<div class="filter-options">
<span class="filter-prefix">&ge;</span>
<button class="filter-btn filter-ordinal-btn" data-field="stability" data-index="0">volatile</button>
<button class="filter-btn filter-ordinal-btn" data-field="stability" data-index="1">revising</button>
<button class="filter-btn filter-ordinal-btn" data-field="stability" data-index="2">fairly stable</button>
<button class="filter-btn filter-ordinal-btn" data-field="stability" data-index="3">stable</button>
<button class="filter-btn filter-ordinal-btn" data-field="stability" data-index="4">established</button>
</div>
</div>
<div class="filter-row filter-row-actions">
<button class="filter-clear-btn">Clear all</button>
</div>
</div>
<div id="search" class="search-panel is-active" data-panel="keyword"></div> <div id="search" class="search-panel is-active" data-panel="keyword"></div>
<p id="search-timing" aria-live="polite"></p> <p id="search-timing" aria-live="polite"></p>

View File

@ -9,14 +9,17 @@
} }
/* ============================================================ /* ============================================================
SORT CONTROLS CONTROLS (sort + filter)
============================================================ */ ============================================================ */
.library-controls { .library-controls {
margin-bottom: 2.5rem;
}
.library-controls-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.6rem; gap: 0.6rem;
margin-bottom: 2.5rem;
} }
.library-controls-label { .library-controls-label {
@ -53,6 +56,160 @@
font-weight: 600; font-weight: 600;
} }
/* Filter toggle */
.library-filter-toggle {
font-family: var(--font-sans);
font-size: 0.75rem;
color: var(--text-muted);
background: none;
border: 1px solid var(--border);
border-radius: 2px;
padding: 0.15em 0.55em;
cursor: pointer;
margin-left: auto;
transition: border-color 0.1s, color 0.1s;
}
.library-filter-toggle:hover,
.library-filter-toggle[aria-expanded="true"] {
border-color: var(--text-muted);
color: var(--text);
}
.filter-toggle-badge {
font-weight: 600;
}
/* ============================================================
FILTER PANEL
============================================================ */
.library-filters {
border-top: 1px solid var(--border);
padding-top: 0.75rem;
margin-top: 0.75rem;
}
.library-filters[hidden] {
display: none;
}
.filter-row {
display: flex;
align-items: center;
gap: 0.45rem;
margin-bottom: 0.45rem;
}
.filter-label {
font-family: var(--font-sans);
font-size: 0.72rem;
color: var(--text-faint);
width: 5.5rem;
flex-shrink: 0;
}
.filter-options {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.25rem;
}
.filter-prefix {
font-family: var(--font-sans);
font-size: 0.72rem;
color: var(--text-faint);
}
.filter-btn {
font-family: var(--font-sans);
font-size: 0.72rem;
color: var(--text-muted);
background: none;
border: 1px solid var(--border);
border-radius: 2px;
padding: 0.1em 0.45em;
cursor: pointer;
transition: border-color 0.1s, color 0.1s;
}
.filter-btn:hover {
border-color: var(--border-muted);
color: var(--text);
}
.filter-btn.is-active {
border-color: var(--text-muted);
color: var(--text);
font-weight: 600;
}
.filter-number {
font-family: var(--font-sans);
font-size: 0.72rem;
width: 3rem;
padding: 0.1em 0.3em;
border: 1px solid var(--border);
border-radius: 2px;
background: transparent;
color: var(--text);
}
.filter-number:focus {
outline: none;
border-color: var(--text-muted);
}
.filter-row-actions {
justify-content: flex-end;
margin-top: 0.25rem;
}
.filter-clear-btn {
font-family: var(--font-sans);
font-size: 0.72rem;
color: var(--text-faint);
background: none;
border: none;
cursor: pointer;
padding: 0;
}
.filter-clear-btn:hover {
color: var(--text);
}
/* Filtered state */
.is-filtered {
display: none !important;
}
/* Search-page result filtering (applied via search-filters.js) */
.search-filtered {
display: none !important;
}
/* Search page filter controls — just the toggle, no sort buttons */
.search-filter-controls {
display: flex;
justify-content: flex-end;
margin-bottom: 0.5rem;
}
/* Empty state message */
.library-empty {
font-family: var(--font-sans);
font-size: var(--text-size-small);
color: var(--text-faint);
font-style: italic;
}
/* ============================================================ /* ============================================================
PORTAL SECTIONS PORTAL SECTIONS
============================================================ */ ============================================================ */

View File

@ -188,6 +188,24 @@
min-width: 230px; min-width: 230px;
} }
/* Epistemic term definition popup shown on hover for filter labels,
metadata strip items, and footer DT elements. */
[data-ep-term] {
cursor: help;
}
.popup-ep-term {
max-width: 320px;
}
.popup-ep-term .popup-source a {
color: inherit;
text-decoration: none;
}
.popup-ep-term .popup-source a:hover {
text-decoration: underline;
}
/* PDF thumbnail popup — first-page image generated by pdftoppm at build time */ /* PDF thumbnail popup — first-page image generated by pdftoppm at build time */
.link-popup:has(.popup-pdf) { .link-popup:has(.popup-pdf) {
padding: 0; padding: 0;

View File

@ -530,6 +530,14 @@ pre code {
margin-top: 0.825rem; /* Half line-height */ margin-top: 0.825rem; /* Half line-height */
} }
/* Paragraphs inside annotation bodies: no first-line indent.
Annotations are short callouts, not body prose; the indent reads as a
typographic glitch when stacked inside the bordered box. */
#markdownBody .annotation-body p + p {
text-indent: 0;
margin-top: 0.825rem;
}
/* ============================================================ /* ============================================================
MATHEMATICS (KaTeX) MATHEMATICS (KaTeX)

View File

@ -56,7 +56,26 @@
}); });
} }
/* Epistemic term definitions concise summaries from the colophon,
shown on hover for filter labels, metadata strip items, etc. */
var EP_DEFS = {
status: 'A controlled vocabulary describing where the work stands: Draft, Working model, Durable, Refined, Superseded, or Deprecated.',
confidence: 'An integer from 0\u2013100, representing the author\u2019s credence in the central thesis.',
importance: 'How much the author thinks this matters, on a 1\u20135 dot scale. Useful for orienting a reader who has limited time.',
evidence: 'How well-evidenced the claims are, on a 1\u20135 scale. High importance and low evidence indicates a speculative position.',
trust: 'A 0\u2013100 score derived automatically from confidence (60%) and evidence quality (40%). Answers \u201chow much should you trust the central claim?\u201d and nothing else.',
scope: 'An orientation from personal to civilizational. Not a rating\u2009\u2014\u2009deliberately not folded into the trust score.',
novelty: 'An orientation from conventional to innovative. Not a rating\u2009\u2014\u2009deliberately not folded into the trust score.',
practicality: 'An orientation from abstract to exceptional. Not a rating\u2009\u2014\u2009deliberately not folded into the trust score.',
stability: 'Auto-computed from git history. Very new or barely-touched documents are volatile; actively-revised are revising; older settled documents are fairly stable, stable, or established.'
};
function bindTargets(root) { function bindTargets(root) {
/* Epistemic term definitions — filter labels, metadata strip, footer */
root.querySelectorAll('[data-ep-term]').forEach(function (el) {
bind(el, epistemicTermContent);
});
/* Citation markers */ /* Citation markers */
root.querySelectorAll('a.cite-link[href^="#ref-"]').forEach(function (el) { root.querySelectorAll('a.cite-link[href^="#ref-"]').forEach(function (el) {
bind(el, citationContent); bind(el, citationContent);
@ -672,6 +691,24 @@
return Promise.resolve(wrap); return Promise.resolve(wrap);
} }
/* Epistemic term definition shows a concise description from the
colophon for any element tagged with data-ep-term="<field>". */
function epistemicTermContent(target) {
var term = target.dataset.epTerm;
var def = term && EP_DEFS[term];
if (!def) return Promise.resolve(null);
var label = term.charAt(0).toUpperCase() + term.slice(1);
return Promise.resolve(
'<div class="popup-ep-term">'
+ '<div class="popup-source" data-popup-source="colophon">'
+ '<a href="/colophon.html#living-documents">Colophon</a>'
+ '</div>'
+ '<div class="popup-title">' + esc(label) + '</div>'
+ '<div class="popup-abstract">' + esc(def) + '</div>'
+ '</div>'
);
}
/* Local PDF shows the build-time first-page thumbnail (.thumb.png). /* Local PDF shows the build-time first-page thumbnail (.thumb.png).
Returns null (no popup) if the thumbnail file does not exist. */ Returns null (no popup) if the thumbnail file does not exist. */
function pdfContent(target) { function pdfContent(target) {

314
static/js/search-filters.js Normal file
View File

@ -0,0 +1,314 @@
/* search-filters.js Epistemic effort filters for the search page.
*
* Loads /data/epistemic-meta.json (a map of URL epistemic fields)
* and hides search results whose source page doesn't match the active
* filters. Works for both Pagefind keyword results and semantic results.
*
* Reuses the same CSS classes and filter-panel markup as library.html
* so the two pages look and behave identically.
*/
(function () {
'use strict';
var KEY = 'search-filter-state';
var SCALES = {
scope: ['personal', 'average', 'broad', 'civilizational'],
novelty: ['conventional', 'moderate', 'idiosyncratic', 'innovative'],
practicality: ['abstract', 'moderate', 'high', 'exceptional'],
stability: ['volatile', 'revising', 'fairly stable', 'stable', 'established']
};
var state = {
status: [],
confidence: null,
importance: null,
evidence: null,
score: null,
scope: null,
novelty: null,
practicality: null,
stability: null
};
var epistemicMeta = null; /* URL → {status, confidence, …} loaded lazily */
/* ---- Persistence ---- */
function load() {
try {
var raw = localStorage.getItem(KEY);
if (raw) {
var obj = JSON.parse(raw);
for (var k in state) {
if (obj.hasOwnProperty(k)) state[k] = obj[k];
}
}
} catch (e) {}
}
function save() {
try { localStorage.setItem(KEY, JSON.stringify(state)); } catch (e) {}
}
/* ---- Metadata loading ---- */
var metaPromise = null;
function loadMeta() {
if (epistemicMeta) return Promise.resolve(epistemicMeta);
if (metaPromise) return metaPromise;
metaPromise = fetch('/data/epistemic-meta.json')
.then(function (r) { return r.ok ? r.json() : {}; })
.catch(function () { return {}; })
.then(function (data) { epistemicMeta = data; return data; });
return metaPromise;
}
/* ---- Filtering logic ---- */
function passes(meta) {
if (!meta) return true; /* no metadata = don't filter out */
if (state.status.length) {
var s = (meta.status || '').toLowerCase();
if (!s || state.status.indexOf(s) === -1) return false;
}
if (state.confidence !== null) {
if (!meta.confidence || +meta.confidence < state.confidence) return false;
}
if (state.importance !== null) {
if (!meta.importance || +meta.importance < state.importance) return false;
}
if (state.evidence !== null) {
if (!meta.evidence || +meta.evidence < state.evidence) return false;
}
if (state.score !== null) {
if (!meta.score || +meta.score < state.score) return false;
}
var ords = ['scope', 'novelty', 'practicality', 'stability'];
for (var i = 0; i < ords.length; i++) {
var k = ords[i];
if (state[k] !== null) {
var v = (meta[k] || '').toLowerCase();
var idx = SCALES[k].indexOf(v);
if (idx === -1 || idx < state[k]) return false;
}
}
return true;
}
function hasActiveFilters() {
if (state.status.length) return true;
var fields = ['confidence', 'importance', 'evidence', 'score',
'scope', 'novelty', 'practicality', 'stability'];
for (var i = 0; i < fields.length; i++) {
if (state[fields[i]] !== null) return true;
}
return false;
}
/* ---- URL extraction ---- */
/* Normalise a URL to a pathname for lookup in epistemicMeta.
Pagefind results use full URLs; semantic results use relative paths. */
function normUrl(href) {
if (!href) return null;
try {
var u = new URL(href, window.location.origin);
return u.pathname;
} catch (e) {
return href;
}
}
/* ---- Apply filters to rendered results ---- */
function applyToPagefind() {
if (!epistemicMeta || !hasActiveFilters()) {
/* Remove any previous filtering */
document.querySelectorAll('.pagefind-ui__result.search-filtered').forEach(function (el) {
el.classList.remove('search-filtered');
});
return;
}
document.querySelectorAll('.pagefind-ui__result').forEach(function (el) {
var link = el.querySelector('.pagefind-ui__result-link');
if (!link) return;
var url = normUrl(link.getAttribute('href'));
var meta = url ? epistemicMeta[url] : null;
el.classList.toggle('search-filtered', !passes(meta));
});
}
function applyToSemantic() {
if (!epistemicMeta || !hasActiveFilters()) {
document.querySelectorAll('.semantic-result.search-filtered').forEach(function (el) {
el.classList.remove('search-filtered');
});
return;
}
document.querySelectorAll('.semantic-result').forEach(function (el) {
var link = el.querySelector('.semantic-result-title');
if (!link) return;
var url = normUrl(link.getAttribute('href'));
var meta = url ? epistemicMeta[url] : null;
el.classList.toggle('search-filtered', !passes(meta));
});
}
function applyFilters() {
applyToPagefind();
applyToSemantic();
syncUI();
save();
}
/* ---- UI sync ---- */
function activeCount() {
var n = 0;
if (state.status.length) n++;
var fields = ['confidence', 'importance', 'evidence', 'score',
'scope', 'novelty', 'practicality', 'stability'];
for (var i = 0; i < fields.length; i++) {
if (state[fields[i]] !== null) n++;
}
return n;
}
function syncUI() {
var badge = document.querySelector('.filter-toggle-badge');
var n = activeCount();
if (badge) badge.textContent = n ? ' (' + n + ')' : '';
document.querySelectorAll('.filter-status-btn').forEach(function (btn) {
btn.classList.toggle('is-active', state.status.indexOf(btn.dataset.value) !== -1);
});
var ci = document.getElementById('filter-confidence');
if (ci) ci.value = state.confidence !== null ? state.confidence : '';
var si = document.getElementById('filter-score');
if (si) si.value = state.score !== null ? state.score : '';
document.querySelectorAll('.filter-threshold-btn').forEach(function (btn) {
btn.classList.toggle('is-active', state[btn.dataset.field] === +btn.dataset.value);
});
document.querySelectorAll('.filter-ordinal-btn').forEach(function (btn) {
btn.classList.toggle('is-active', state[btn.dataset.field] === +btn.dataset.index);
});
}
/* ---- Init ---- */
document.addEventListener('DOMContentLoaded', function () {
load();
var panel = document.getElementById('search-filters');
var toggle = document.querySelector('.library-filter-toggle');
if (activeCount() > 0 && panel && toggle) {
panel.hidden = false;
toggle.setAttribute('aria-expanded', 'true');
}
syncUI();
/* Load metadata eagerly if filters are active */
if (hasActiveFilters()) {
loadMeta().then(applyFilters);
}
/* Toggle panel */
if (toggle && panel) {
toggle.addEventListener('click', function () {
var opening = panel.hidden;
panel.hidden = !opening;
toggle.setAttribute('aria-expanded', opening ? 'true' : 'false');
});
}
/* Status buttons */
document.querySelectorAll('.filter-status-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
var v = btn.dataset.value;
var i = state.status.indexOf(v);
if (i === -1) state.status.push(v);
else state.status.splice(i, 1);
loadMeta().then(applyFilters);
});
});
/* Threshold buttons (importance, evidence) */
document.querySelectorAll('.filter-threshold-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
var f = btn.dataset.field;
var v = +btn.dataset.value;
state[f] = (state[f] === v) ? null : v;
loadMeta().then(applyFilters);
});
});
/* Ordinal buttons (scope, novelty, practicality, stability) */
document.querySelectorAll('.filter-ordinal-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
var f = btn.dataset.field;
var idx = +btn.dataset.index;
state[f] = (state[f] === idx) ? null : idx;
loadMeta().then(applyFilters);
});
});
/* Number inputs (confidence, trust/score) */
['confidence', 'score'].forEach(function (field) {
var el = document.getElementById('filter-' + (field === 'score' ? 'score' : field));
if (!el) return;
el.addEventListener('input', function () {
var v = el.value.trim();
state[field] = v !== '' ? Math.max(0, Math.min(100, parseInt(v, 10) || 0)) : null;
loadMeta().then(applyFilters);
});
});
/* Clear all */
var clearBtn = document.querySelector('.filter-clear-btn');
if (clearBtn) {
clearBtn.addEventListener('click', function () {
state.status = [];
state.confidence = null;
state.importance = null;
state.evidence = null;
state.score = null;
state.scope = null;
state.novelty = null;
state.practicality = null;
state.stability = null;
applyFilters();
});
}
/* Observe Pagefind result changes to re-apply filters.
Pagefind dynamically rebuilds the results container. */
var searchEl = document.getElementById('search');
if (searchEl) {
new MutationObserver(function () {
if (hasActiveFilters() && epistemicMeta) {
applyToPagefind();
}
}).observe(searchEl, { childList: true, subtree: true });
}
/* Observe semantic results container */
var semanticEl = document.getElementById('semantic-results');
if (semanticEl) {
new MutationObserver(function () {
if (hasActiveFilters() && epistemicMeta) {
applyToSemantic();
}
}).observe(semanticEl, { childList: true, subtree: true });
}
});
}());

View File

@ -10,6 +10,7 @@ $if(search)$
<script src="/pagefind/pagefind-ui.js"></script> <script src="/pagefind/pagefind-ui.js"></script>
<script src="/js/search.js" defer></script> <script src="/js/search.js" defer></script>
<script src="/js/semantic-search.js" defer></script> <script src="/js/semantic-search.js" defer></script>
<script src="/js/search-filters.js" defer></script>
$endif$ $endif$
$body$ $body$
$partial("templates/partials/footer.html")$ $partial("templates/partials/footer.html")$

View File

@ -3,19 +3,119 @@
<p class="library-intro">Everything on this site, organized by portal.</p> <p class="library-intro">Everything on this site, organized by portal.</p>
<div class="library-controls"> <div class="library-controls">
<span class="library-controls-label">Sort by</span> <div class="library-controls-row">
<div class="library-controls-options" role="group" aria-label="Sort order"> <span class="library-controls-label">Sort by</span>
<button class="library-sort-btn" data-sort="date">date</button> <div class="library-controls-options" role="group" aria-label="Sort order">
<button class="library-sort-btn" data-sort="title">title</button> <button class="library-sort-btn" data-sort="date">date</button>
<button class="library-sort-btn" data-sort="score">trust</button> <button class="library-sort-btn" data-sort="title">title</button>
<button class="library-sort-btn" data-sort="score">trust</button>
</div>
<button class="library-filter-toggle" aria-expanded="false" aria-controls="library-filters">
Filters<span class="filter-toggle-badge"></span>
</button>
</div>
<div id="library-filters" class="library-filters" hidden>
<div class="filter-row">
<span class="filter-label" data-ep-term="status">status</span>
<div class="filter-options">
<button class="filter-btn filter-status-btn" data-value="draft">draft</button>
<button class="filter-btn filter-status-btn" data-value="working model">working model</button>
<button class="filter-btn filter-status-btn" data-value="durable">durable</button>
<button class="filter-btn filter-status-btn" data-value="refined">refined</button>
<button class="filter-btn filter-status-btn" data-value="superseded">superseded</button>
<button class="filter-btn filter-status-btn" data-value="deprecated">deprecated</button>
</div>
</div>
<div class="filter-row">
<span class="filter-label" data-ep-term="confidence">confidence</span>
<div class="filter-options">
<span class="filter-prefix">&ge;</span>
<input type="number" id="filter-confidence" class="filter-number" min="0" max="100" placeholder="&mdash;" aria-label="Minimum confidence" />
</div>
</div>
<div class="filter-row">
<span class="filter-label" data-ep-term="importance">importance</span>
<div class="filter-options">
<span class="filter-prefix">&ge;</span>
<button class="filter-btn filter-threshold-btn" data-field="importance" data-value="1">1</button>
<button class="filter-btn filter-threshold-btn" data-field="importance" data-value="2">2</button>
<button class="filter-btn filter-threshold-btn" data-field="importance" data-value="3">3</button>
<button class="filter-btn filter-threshold-btn" data-field="importance" data-value="4">4</button>
<button class="filter-btn filter-threshold-btn" data-field="importance" data-value="5">5</button>
</div>
</div>
<div class="filter-row">
<span class="filter-label" data-ep-term="evidence">evidence</span>
<div class="filter-options">
<span class="filter-prefix">&ge;</span>
<button class="filter-btn filter-threshold-btn" data-field="evidence" data-value="1">1</button>
<button class="filter-btn filter-threshold-btn" data-field="evidence" data-value="2">2</button>
<button class="filter-btn filter-threshold-btn" data-field="evidence" data-value="3">3</button>
<button class="filter-btn filter-threshold-btn" data-field="evidence" data-value="4">4</button>
<button class="filter-btn filter-threshold-btn" data-field="evidence" data-value="5">5</button>
</div>
</div>
<div class="filter-row">
<span class="filter-label" data-ep-term="trust">trust</span>
<div class="filter-options">
<span class="filter-prefix">&ge;</span>
<input type="number" id="filter-score" class="filter-number" min="0" max="100" placeholder="&mdash;" aria-label="Minimum trust score" />
</div>
</div>
<div class="filter-row">
<span class="filter-label" data-ep-term="scope">scope</span>
<div class="filter-options">
<span class="filter-prefix">&ge;</span>
<button class="filter-btn filter-ordinal-btn" data-field="scope" data-index="0">personal</button>
<button class="filter-btn filter-ordinal-btn" data-field="scope" data-index="1">average</button>
<button class="filter-btn filter-ordinal-btn" data-field="scope" data-index="2">broad</button>
<button class="filter-btn filter-ordinal-btn" data-field="scope" data-index="3">civilizational</button>
</div>
</div>
<div class="filter-row">
<span class="filter-label" data-ep-term="novelty">novelty</span>
<div class="filter-options">
<span class="filter-prefix">&ge;</span>
<button class="filter-btn filter-ordinal-btn" data-field="novelty" data-index="0">conventional</button>
<button class="filter-btn filter-ordinal-btn" data-field="novelty" data-index="1">moderate</button>
<button class="filter-btn filter-ordinal-btn" data-field="novelty" data-index="2">idiosyncratic</button>
<button class="filter-btn filter-ordinal-btn" data-field="novelty" data-index="3">innovative</button>
</div>
</div>
<div class="filter-row">
<span class="filter-label" data-ep-term="practicality">practicality</span>
<div class="filter-options">
<span class="filter-prefix">&ge;</span>
<button class="filter-btn filter-ordinal-btn" data-field="practicality" data-index="0">abstract</button>
<button class="filter-btn filter-ordinal-btn" data-field="practicality" data-index="1">moderate</button>
<button class="filter-btn filter-ordinal-btn" data-field="practicality" data-index="2">high</button>
<button class="filter-btn filter-ordinal-btn" data-field="practicality" data-index="3">exceptional</button>
</div>
</div>
<div class="filter-row">
<span class="filter-label" data-ep-term="stability">stability</span>
<div class="filter-options">
<span class="filter-prefix">&ge;</span>
<button class="filter-btn filter-ordinal-btn" data-field="stability" data-index="0">volatile</button>
<button class="filter-btn filter-ordinal-btn" data-field="stability" data-index="1">revising</button>
<button class="filter-btn filter-ordinal-btn" data-field="stability" data-index="2">fairly stable</button>
<button class="filter-btn filter-ordinal-btn" data-field="stability" data-index="3">stable</button>
<button class="filter-btn filter-ordinal-btn" data-field="stability" data-index="4">established</button>
</div>
</div>
<div class="filter-row filter-row-actions">
<button class="filter-clear-btn">Clear all</button>
</div>
</div> </div>
</div> </div>
<p class="library-empty is-filtered">No entries match the current filters.</p>
$if(research-entries)$ $if(research-entries)$
<section class="library-section"> <section class="library-section">
<h2 id="research"><a href="/research/">Research</a></h2> <h2 id="research"><a href="/research/">Research</a></h2>
<ul class="library-list">$for(research-entries)$ <ul class="library-list">$for(research-entries)$
<li class="library-entry" data-date="$date-iso$"$if(overall-score)$ data-score="$overall-score$"$endif$> <li class="library-entry" data-date="$date-iso$"$if(overall-score)$ data-score="$overall-score$"$endif$$if(status)$ data-status="$status$"$endif$$if(confidence)$ data-confidence="$confidence$"$endif$$if(importance)$ data-importance="$importance$"$endif$$if(evidence)$ data-evidence="$evidence$"$endif$$if(scope)$ data-scope="$scope$"$endif$$if(novelty)$ data-novelty="$novelty$"$endif$$if(practicality)$ data-practicality="$practicality$"$endif$$if(stability)$ data-stability="$stability$"$endif$>
<div class="library-entry-header"> <div class="library-entry-header">
<a class="library-entry-title" href="$url$">$title$</a> <a class="library-entry-title" href="$url$">$title$</a>
<span class="library-entry-date">$date-created$</span> <span class="library-entry-date">$date-created$</span>
@ -29,7 +129,7 @@ $if(nonfiction-entries)$
<section class="library-section"> <section class="library-section">
<h2 id="nonfiction"><a href="/nonfiction/">Nonfiction</a></h2> <h2 id="nonfiction"><a href="/nonfiction/">Nonfiction</a></h2>
<ul class="library-list">$for(nonfiction-entries)$ <ul class="library-list">$for(nonfiction-entries)$
<li class="library-entry" data-date="$date-iso$"$if(overall-score)$ data-score="$overall-score$"$endif$> <li class="library-entry" data-date="$date-iso$"$if(overall-score)$ data-score="$overall-score$"$endif$$if(status)$ data-status="$status$"$endif$$if(confidence)$ data-confidence="$confidence$"$endif$$if(importance)$ data-importance="$importance$"$endif$$if(evidence)$ data-evidence="$evidence$"$endif$$if(scope)$ data-scope="$scope$"$endif$$if(novelty)$ data-novelty="$novelty$"$endif$$if(practicality)$ data-practicality="$practicality$"$endif$$if(stability)$ data-stability="$stability$"$endif$>
<div class="library-entry-header"> <div class="library-entry-header">
<a class="library-entry-title" href="$url$">$title$</a> <a class="library-entry-title" href="$url$">$title$</a>
<span class="library-entry-date">$date-created$</span> <span class="library-entry-date">$date-created$</span>
@ -43,7 +143,7 @@ $if(fiction-entries)$
<section class="library-section"> <section class="library-section">
<h2 id="fiction"><a href="/fiction/">Fiction</a></h2> <h2 id="fiction"><a href="/fiction/">Fiction</a></h2>
<ul class="library-list">$for(fiction-entries)$ <ul class="library-list">$for(fiction-entries)$
<li class="library-entry" data-date="$date-iso$"$if(overall-score)$ data-score="$overall-score$"$endif$> <li class="library-entry" data-date="$date-iso$"$if(overall-score)$ data-score="$overall-score$"$endif$$if(status)$ data-status="$status$"$endif$$if(confidence)$ data-confidence="$confidence$"$endif$$if(importance)$ data-importance="$importance$"$endif$$if(evidence)$ data-evidence="$evidence$"$endif$$if(scope)$ data-scope="$scope$"$endif$$if(novelty)$ data-novelty="$novelty$"$endif$$if(practicality)$ data-practicality="$practicality$"$endif$$if(stability)$ data-stability="$stability$"$endif$>
<div class="library-entry-header"> <div class="library-entry-header">
<a class="library-entry-title" href="$url$">$title$</a> <a class="library-entry-title" href="$url$">$title$</a>
<span class="library-entry-date">$date-created$</span> <span class="library-entry-date">$date-created$</span>
@ -57,7 +157,7 @@ $if(poetry-entries)$
<section class="library-section"> <section class="library-section">
<h2 id="poetry"><a href="/poetry/">Poetry</a></h2> <h2 id="poetry"><a href="/poetry/">Poetry</a></h2>
<ul class="library-list">$for(poetry-entries)$ <ul class="library-list">$for(poetry-entries)$
<li class="library-entry" data-date="$date-iso$"$if(overall-score)$ data-score="$overall-score$"$endif$> <li class="library-entry" data-date="$date-iso$"$if(overall-score)$ data-score="$overall-score$"$endif$$if(status)$ data-status="$status$"$endif$$if(confidence)$ data-confidence="$confidence$"$endif$$if(importance)$ data-importance="$importance$"$endif$$if(evidence)$ data-evidence="$evidence$"$endif$$if(scope)$ data-scope="$scope$"$endif$$if(novelty)$ data-novelty="$novelty$"$endif$$if(practicality)$ data-practicality="$practicality$"$endif$$if(stability)$ data-stability="$stability$"$endif$>
<div class="library-entry-header"> <div class="library-entry-header">
<a class="library-entry-title" href="$url$">$title$</a> <a class="library-entry-title" href="$url$">$title$</a>
<span class="library-entry-date">$date-created$</span> <span class="library-entry-date">$date-created$</span>
@ -71,7 +171,7 @@ $if(music-entries)$
<section class="library-section"> <section class="library-section">
<h2 id="music"><a href="/music/">Music</a></h2> <h2 id="music"><a href="/music/">Music</a></h2>
<ul class="library-list">$for(music-entries)$ <ul class="library-list">$for(music-entries)$
<li class="library-entry" data-date="$date-iso$"$if(overall-score)$ data-score="$overall-score$"$endif$> <li class="library-entry" data-date="$date-iso$"$if(overall-score)$ data-score="$overall-score$"$endif$$if(status)$ data-status="$status$"$endif$$if(confidence)$ data-confidence="$confidence$"$endif$$if(importance)$ data-importance="$importance$"$endif$$if(evidence)$ data-evidence="$evidence$"$endif$$if(scope)$ data-scope="$scope$"$endif$$if(novelty)$ data-novelty="$novelty$"$endif$$if(practicality)$ data-practicality="$practicality$"$endif$$if(stability)$ data-stability="$stability$"$endif$>
<div class="library-entry-header"> <div class="library-entry-header">
<a class="library-entry-title" href="$url$">$title$</a> <a class="library-entry-title" href="$url$">$title$</a>
<span class="library-entry-date">$date-created$</span> <span class="library-entry-date">$date-created$</span>
@ -85,7 +185,7 @@ $if(ai-entries)$
<section class="library-section"> <section class="library-section">
<h2 id="ai"><a href="/ai/">AI</a></h2> <h2 id="ai"><a href="/ai/">AI</a></h2>
<ul class="library-list">$for(ai-entries)$ <ul class="library-list">$for(ai-entries)$
<li class="library-entry" data-date="$date-iso$"$if(overall-score)$ data-score="$overall-score$"$endif$> <li class="library-entry" data-date="$date-iso$"$if(overall-score)$ data-score="$overall-score$"$endif$$if(status)$ data-status="$status$"$endif$$if(confidence)$ data-confidence="$confidence$"$endif$$if(importance)$ data-importance="$importance$"$endif$$if(evidence)$ data-evidence="$evidence$"$endif$$if(scope)$ data-scope="$scope$"$endif$$if(novelty)$ data-novelty="$novelty$"$endif$$if(practicality)$ data-practicality="$practicality$"$endif$$if(stability)$ data-stability="$stability$"$endif$>
<div class="library-entry-header"> <div class="library-entry-header">
<a class="library-entry-title" href="$url$">$title$</a> <a class="library-entry-title" href="$url$">$title$</a>
<span class="library-entry-date">$date-created$</span> <span class="library-entry-date">$date-created$</span>
@ -99,7 +199,7 @@ $if(tech-entries)$
<section class="library-section"> <section class="library-section">
<h2 id="tech"><a href="/tech/">Tech</a></h2> <h2 id="tech"><a href="/tech/">Tech</a></h2>
<ul class="library-list">$for(tech-entries)$ <ul class="library-list">$for(tech-entries)$
<li class="library-entry" data-date="$date-iso$"$if(overall-score)$ data-score="$overall-score$"$endif$> <li class="library-entry" data-date="$date-iso$"$if(overall-score)$ data-score="$overall-score$"$endif$$if(status)$ data-status="$status$"$endif$$if(confidence)$ data-confidence="$confidence$"$endif$$if(importance)$ data-importance="$importance$"$endif$$if(evidence)$ data-evidence="$evidence$"$endif$$if(scope)$ data-scope="$scope$"$endif$$if(novelty)$ data-novelty="$novelty$"$endif$$if(practicality)$ data-practicality="$practicality$"$endif$$if(stability)$ data-stability="$stability$"$endif$>
<div class="library-entry-header"> <div class="library-entry-header">
<a class="library-entry-title" href="$url$">$title$</a> <a class="library-entry-title" href="$url$">$title$</a>
<span class="library-entry-date">$date-created$</span> <span class="library-entry-date">$date-created$</span>
@ -113,7 +213,7 @@ $if(miscellany-entries)$
<section class="library-section"> <section class="library-section">
<h2 id="miscellany"><a href="/miscellany/">Miscellany</a></h2> <h2 id="miscellany"><a href="/miscellany/">Miscellany</a></h2>
<ul class="library-list">$for(miscellany-entries)$ <ul class="library-list">$for(miscellany-entries)$
<li class="library-entry" data-date="$date-iso$"$if(overall-score)$ data-score="$overall-score$"$endif$> <li class="library-entry" data-date="$date-iso$"$if(overall-score)$ data-score="$overall-score$"$endif$$if(status)$ data-status="$status$"$endif$$if(confidence)$ data-confidence="$confidence$"$endif$$if(importance)$ data-importance="$importance$"$endif$$if(evidence)$ data-evidence="$evidence$"$endif$$if(scope)$ data-scope="$scope$"$endif$$if(novelty)$ data-novelty="$novelty$"$endif$$if(practicality)$ data-practicality="$practicality$"$endif$$if(stability)$ data-stability="$stability$"$endif$>
<div class="library-entry-header"> <div class="library-entry-header">
<a class="library-entry-title" href="$url$">$title$</a> <a class="library-entry-title" href="$url$">$title$</a>
<span class="library-entry-date">$date-created$</span> <span class="library-entry-date">$date-created$</span>
@ -126,60 +226,264 @@ $endif$
</div> </div>
<script> <script>
(function () { (function () {
var STORAGE_KEY = 'library-sort'; 'use strict';
var DEFAULT = 'date';
var MODES = { date: 1, title: 1, score: 1 };
function titleOf(entry) { var KEY = 'library-state';
var el = entry.querySelector('.library-entry-title'); var SORT_MODES = { date: 1, title: 1, score: 1 };
var SCALES = {
scope: ['personal', 'average', 'broad', 'civilizational'],
novelty: ['conventional', 'moderate', 'idiosyncratic', 'innovative'],
practicality: ['abstract', 'moderate', 'high', 'exceptional'],
stability: ['volatile', 'revising', 'fairly stable', 'stable', 'established']
};
var state = {
sort: 'date',
status: [],
confidence: null,
importance: null,
evidence: null,
score: null,
scope: null,
novelty: null,
practicality: null,
stability: null
};
/* ---- Persistence ---- */
function load() {
try {
var raw = localStorage.getItem(KEY);
if (raw) {
var obj = JSON.parse(raw);
for (var k in state) {
if (obj.hasOwnProperty(k)) state[k] = obj[k];
}
} else {
var old = localStorage.getItem('library-sort');
if (old && SORT_MODES[old]) state.sort = old;
}
} catch (e) {}
}
function save() {
try {
localStorage.setItem(KEY, JSON.stringify(state));
localStorage.removeItem('library-sort');
} catch (e) {}
}
/* ---- Filtering ---- */
function passes(entry) {
var d = entry.dataset;
if (state.status.length) {
var s = (d.status || '').toLowerCase();
if (!s || state.status.indexOf(s) === -1) return false;
}
if (state.confidence !== null) {
if (!d.confidence || +d.confidence < state.confidence) return false;
}
if (state.importance !== null) {
if (!d.importance || +d.importance < state.importance) return false;
}
if (state.evidence !== null) {
if (!d.evidence || +d.evidence < state.evidence) return false;
}
if (state.score !== null) {
if (!d.score || +d.score < state.score) return false;
}
var ords = ['scope', 'novelty', 'practicality', 'stability'];
for (var i = 0; i < ords.length; i++) {
var k = ords[i];
if (state[k] !== null) {
var v = (d[k] || '').toLowerCase();
var idx = SCALES[k].indexOf(v);
if (idx === -1 || idx < state[k]) return false;
}
}
return true;
}
/* ---- Sorting ---- */
function titleOf(e) {
var el = e.querySelector('.library-entry-title');
return el ? el.textContent.trim().toLowerCase() : ''; return el ? el.textContent.trim().toLowerCase() : '';
} }
function compare(a, b, mode) { function compare(a, b) {
if (mode === 'title') { var m = state.sort;
return titleOf(a).localeCompare(titleOf(b)); if (m === 'title') return titleOf(a).localeCompare(titleOf(b));
} if (m === 'score') {
if (mode === 'score') { var hA = a.dataset.score !== undefined;
// Entries without a score sink to the bottom; ties broken by date desc. var hB = b.dataset.score !== undefined;
var hasA = a.dataset.score !== undefined; if (hA && !hB) return -1;
var hasB = b.dataset.score !== undefined; if (!hA && hB) return 1;
if (hasA && !hasB) return -1; if (hA && hB) {
if (!hasA && hasB) return 1;
if (hasA && hasB) {
var diff = Number(b.dataset.score) - Number(a.dataset.score); var diff = Number(b.dataset.score) - Number(a.dataset.score);
if (diff !== 0) return diff; if (diff !== 0) return diff;
} }
// fall through to date-desc tiebreak
} }
// date desc (ISO strings sort lexicographically)
var da = a.dataset.date || ''; var da = a.dataset.date || '';
var db = b.dataset.date || ''; var db = b.dataset.date || '';
if (da < db) return 1; return da < db ? 1 : da > db ? -1 : 0;
if (da > db) return -1;
return 0;
} }
function applySort(mode) { /* ---- Apply ---- */
if (!MODES[mode]) mode = DEFAULT;
function apply() {
if (!SORT_MODES[state.sort]) state.sort = 'date';
document.querySelectorAll('.library-entry').forEach(function (e) {
e.classList.toggle('is-filtered', !passes(e));
});
document.querySelectorAll('.library-list').forEach(function (list) { document.querySelectorAll('.library-list').forEach(function (list) {
var entries = Array.prototype.slice.call(list.querySelectorAll('.library-entry')); var entries = [].slice.call(list.querySelectorAll('.library-entry'));
entries.sort(function (a, b) { return compare(a, b, mode); }); entries.sort(compare);
entries.forEach(function (el) { list.appendChild(el); }); entries.forEach(function (e) { list.appendChild(e); });
}); });
document.querySelectorAll('.library-sort-btn').forEach(function (btn) {
btn.classList.toggle('is-active', btn.dataset.sort === mode); document.querySelectorAll('.library-section').forEach(function (sec) {
sec.classList.toggle('is-filtered', !sec.querySelector('.library-entry:not(.is-filtered)'));
}); });
try { localStorage.setItem(STORAGE_KEY, mode); } catch (e) {}
var any = document.querySelector('.library-section:not(.is-filtered)');
var msg = document.querySelector('.library-empty');
if (msg) msg.classList.toggle('is-filtered', !!any);
syncUI();
save();
} }
/* ---- UI sync ---- */
function activeCount() {
var n = 0;
if (state.status.length) n++;
var fields = ['confidence', 'importance', 'evidence', 'score',
'scope', 'novelty', 'practicality', 'stability'];
for (var i = 0; i < fields.length; i++) {
if (state[fields[i]] !== null) n++;
}
return n;
}
function syncUI() {
document.querySelectorAll('.library-sort-btn').forEach(function (btn) {
btn.classList.toggle('is-active', btn.dataset.sort === state.sort);
});
var badge = document.querySelector('.filter-toggle-badge');
var n = activeCount();
if (badge) badge.textContent = n ? ' (' + n + ')' : '';
document.querySelectorAll('.filter-status-btn').forEach(function (btn) {
btn.classList.toggle('is-active', state.status.indexOf(btn.dataset.value) !== -1);
});
var ci = document.getElementById('filter-confidence');
if (ci) ci.value = state.confidence !== null ? state.confidence : '';
var si = document.getElementById('filter-score');
if (si) si.value = state.score !== null ? state.score : '';
document.querySelectorAll('.filter-threshold-btn').forEach(function (btn) {
btn.classList.toggle('is-active', state[btn.dataset.field] === +btn.dataset.value);
});
document.querySelectorAll('.filter-ordinal-btn').forEach(function (btn) {
btn.classList.toggle('is-active', state[btn.dataset.field] === +btn.dataset.index);
});
}
/* ---- Init ---- */
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
var saved; load();
try { saved = localStorage.getItem(STORAGE_KEY); } catch (e) {} apply();
applySort(saved || DEFAULT);
var panel = document.getElementById('library-filters');
var toggle = document.querySelector('.library-filter-toggle');
if (activeCount() > 0 && panel && toggle) {
panel.hidden = false;
toggle.setAttribute('aria-expanded', 'true');
}
document.querySelectorAll('.library-sort-btn').forEach(function (btn) { document.querySelectorAll('.library-sort-btn').forEach(function (btn) {
btn.addEventListener('click', function () { applySort(btn.dataset.sort); }); btn.addEventListener('click', function () {
state.sort = btn.dataset.sort;
apply();
});
}); });
if (toggle && panel) {
toggle.addEventListener('click', function () {
var opening = panel.hidden;
panel.hidden = !opening;
toggle.setAttribute('aria-expanded', opening ? 'true' : 'false');
});
}
document.querySelectorAll('.filter-status-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
var v = btn.dataset.value;
var i = state.status.indexOf(v);
if (i === -1) state.status.push(v);
else state.status.splice(i, 1);
apply();
});
});
document.querySelectorAll('.filter-threshold-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
var f = btn.dataset.field;
var v = +btn.dataset.value;
state[f] = (state[f] === v) ? null : v;
apply();
});
});
document.querySelectorAll('.filter-ordinal-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
var f = btn.dataset.field;
var idx = +btn.dataset.index;
state[f] = (state[f] === idx) ? null : idx;
apply();
});
});
['confidence', 'score'].forEach(function (field) {
var el = document.getElementById('filter-' + (field === 'score' ? 'score' : field));
if (!el) return;
el.addEventListener('input', function () {
var v = el.value.trim();
state[field] = v !== '' ? Math.max(0, Math.min(100, parseInt(v, 10) || 0)) : null;
apply();
});
});
var clearBtn = document.querySelector('.filter-clear-btn');
if (clearBtn) {
clearBtn.addEventListener('click', function () {
state.status = [];
state.confidence = null;
state.importance = null;
state.evidence = null;
state.score = null;
state.scope = null;
state.novelty = null;
state.practicality = null;
state.stability = null;
apply();
});
}
}); });
}()); }());
</script> </script>

View File

@ -14,6 +14,7 @@ $if(home)$<title>Levi Neuwirth</title>$else$<title>$title$ — Levi Neuwirth</ti
<link rel="stylesheet" href="/css/images.css"> <link rel="stylesheet" href="/css/images.css">
$if(home)$<link rel="stylesheet" href="/css/home.css">$endif$ $if(home)$<link rel="stylesheet" href="/css/home.css">$endif$
$if(library)$<link rel="stylesheet" href="/css/library.css">$endif$ $if(library)$<link rel="stylesheet" href="/css/library.css">$endif$
$if(search)$<link rel="stylesheet" href="/css/library.css">$endif$
$if(new-page)$<link rel="stylesheet" href="/css/new.css">$endif$ $if(new-page)$<link rel="stylesheet" href="/css/new.css">$endif$
$if(memento-mori)$<link rel="stylesheet" href="/css/memento-mori.css">$endif$ $if(memento-mori)$<link rel="stylesheet" href="/css/memento-mori.css">$endif$
$if(catalog)$<link rel="stylesheet" href="/css/catalog.css">$endif$ $if(catalog)$<link rel="stylesheet" href="/css/catalog.css">$endif$

View File

@ -19,14 +19,14 @@
$endif$ $endif$
$if(status)$ $if(status)$
<div class="meta-row meta-epistemic-strip" data-pagefind-ignore="all"> <div class="meta-row meta-epistemic-strip" data-pagefind-ignore="all">
$if(overall-score)$<span class="ep-trust" title="Trust score: $overall-score$/100 (confidence × 0.6 + evidence × 0.4)"><span class="ep-score">$overall-score$%</span> trust</span>$endif$ $if(overall-score)$<span class="ep-trust" data-ep-term="trust"><span class="ep-score">$overall-score$%</span> trust</span>$endif$
<span class="ep-status">$status$</span> <span class="ep-status" data-ep-term="status">$status$</span>
$if(confidence)$<span class="ep-row" title="Confidence">$confidence$% confidence</span>$endif$ $if(confidence)$<span class="ep-row" data-ep-term="confidence">$confidence$% confidence</span>$endif$
$if(importance-dots)$<span class="ep-row" title="Importance: $importance$/5"><span class="ep-dots">$importance-dots$</span> importance</span>$endif$ $if(importance-dots)$<span class="ep-row" data-ep-term="importance"><span class="ep-dots">$importance-dots$</span> importance</span>$endif$
$if(evidence-dots)$<span class="ep-row" title="Evidence quality: $evidence$/5"><span class="ep-dots">$evidence-dots$</span> evidence quality</span>$endif$ $if(evidence-dots)$<span class="ep-row" data-ep-term="evidence"><span class="ep-dots">$evidence-dots$</span> evidence quality</span>$endif$
$if(scope)$<span class="ep-row" title="Scope">$scope$ scope</span>$endif$ $if(scope)$<span class="ep-row" data-ep-term="scope">$scope$ scope</span>$endif$
$if(novelty)$<span class="ep-row" title="Novelty">$novelty$ novelty</span>$endif$ $if(novelty)$<span class="ep-row" data-ep-term="novelty">$novelty$ novelty</span>$endif$
$if(practicality)$<span class="ep-row" title="Practicality">$practicality$ practicality</span>$endif$ $if(practicality)$<span class="ep-row" data-ep-term="practicality">$practicality$ practicality</span>$endif$
</div> </div>
$endif$ $endif$
<nav class="meta-row meta-pagelinks" aria-label="Page sections"> <nav class="meta-row meta-pagelinks" aria-label="Page sections">

View File

@ -36,9 +36,9 @@
<div class="meta-footer-section meta-footer-epistemic" id="epistemic"> <div class="meta-footer-section meta-footer-epistemic" id="epistemic">
<h3><a href="/colophon.html#living-documents">Epistemic</a></h3> <h3><a href="/colophon.html#living-documents">Epistemic</a></h3>
<dl class="ep-expanded"> <dl class="ep-expanded">
<dt>Stability</dt><dd>$stability$</dd> <dt data-ep-term="stability">Stability</dt><dd>$stability$</dd>
$if(last-reviewed)$<dt>Last reviewed</dt><dd>$last-reviewed$</dd>$endif$ $if(last-reviewed)$<dt>Last reviewed</dt><dd>$last-reviewed$</dd>$endif$
$if(confidence-trend)$<dt>Confidence trend</dt><dd>$confidence-trend$</dd>$endif$ $if(confidence-trend)$<dt data-ep-term="confidence">Confidence trend</dt><dd>$confidence-trend$</dd>$endif$
</dl> </dl>
</div> </div>
$endif$ $endif$