filters + epistemic popups
This commit is contained in:
parent
56d9262585
commit
e25a311dd9
|
|
@ -4,11 +4,13 @@ module Site (rules) where
|
|||
|
||||
import Control.Monad (filterM, when)
|
||||
import Data.List (isPrefixOf)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Maybe (catMaybes, fromMaybe)
|
||||
import System.Environment (lookupEnv)
|
||||
import System.FilePath (takeDirectory, takeFileName, replaceExtension)
|
||||
import Text.Read (readMaybe)
|
||||
import qualified Data.Aeson as Aeson
|
||||
import qualified Data.ByteString.Lazy.Char8 as LBS
|
||||
import qualified Data.Map.Strict as Map
|
||||
import Hakyll
|
||||
import Authors (buildAllAuthors, applyAuthorRules)
|
||||
import Backlinks (backlinkRules)
|
||||
|
|
@ -445,6 +447,24 @@ rules = do
|
|||
let urls = [ "/" ++ r | Just r <- routes ]
|
||||
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
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
|
@ -485,3 +505,48 @@ rules = do
|
|||
<> bodyField "description"
|
||||
<> defaultContext
|
||||
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
|
||||
|
|
|
|||
|
|
@ -8,6 +8,105 @@ search: true
|
|||
<button class="search-tab" data-tab="semantic" role="tab" aria-selected="false">Semantic</button>
|
||||
</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">≥</span>
|
||||
<input type="number" id="filter-confidence" class="filter-number" min="0" max="100" placeholder="—" 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">≥</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">≥</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">≥</span>
|
||||
<input type="number" id="filter-score" class="filter-number" min="0" max="100" placeholder="—" 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">≥</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">≥</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">≥</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">≥</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>
|
||||
<p id="search-timing" aria-live="polite"></p>
|
||||
|
||||
|
|
|
|||
|
|
@ -9,14 +9,17 @@
|
|||
}
|
||||
|
||||
/* ============================================================
|
||||
SORT CONTROLS
|
||||
CONTROLS (sort + filter)
|
||||
============================================================ */
|
||||
|
||||
.library-controls {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.library-controls-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.library-controls-label {
|
||||
|
|
@ -53,6 +56,160 @@
|
|||
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
|
||||
============================================================ */
|
||||
|
|
|
|||
|
|
@ -188,6 +188,24 @@
|
|||
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 */
|
||||
.link-popup:has(.popup-pdf) {
|
||||
padding: 0;
|
||||
|
|
|
|||
|
|
@ -530,6 +530,14 @@ pre code {
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
/* Epistemic term definitions — filter labels, metadata strip, footer */
|
||||
root.querySelectorAll('[data-ep-term]').forEach(function (el) {
|
||||
bind(el, epistemicTermContent);
|
||||
});
|
||||
|
||||
/* Citation markers */
|
||||
root.querySelectorAll('a.cite-link[href^="#ref-"]').forEach(function (el) {
|
||||
bind(el, citationContent);
|
||||
|
|
@ -672,6 +691,24 @@
|
|||
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).
|
||||
Returns null (no popup) if the thumbnail file does not exist. */
|
||||
function pdfContent(target) {
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
});
|
||||
}());
|
||||
|
|
@ -10,6 +10,7 @@ $if(search)$
|
|||
<script src="/pagefind/pagefind-ui.js"></script>
|
||||
<script src="/js/search.js" defer></script>
|
||||
<script src="/js/semantic-search.js" defer></script>
|
||||
<script src="/js/search-filters.js" defer></script>
|
||||
$endif$
|
||||
$body$
|
||||
$partial("templates/partials/footer.html")$
|
||||
|
|
|
|||
|
|
@ -3,19 +3,119 @@
|
|||
<p class="library-intro">Everything on this site, organized by portal.</p>
|
||||
|
||||
<div class="library-controls">
|
||||
<span class="library-controls-label">Sort by</span>
|
||||
<div class="library-controls-options" role="group" aria-label="Sort order">
|
||||
<button class="library-sort-btn" data-sort="date">date</button>
|
||||
<button class="library-sort-btn" data-sort="title">title</button>
|
||||
<button class="library-sort-btn" data-sort="score">trust</button>
|
||||
<div class="library-controls-row">
|
||||
<span class="library-controls-label">Sort by</span>
|
||||
<div class="library-controls-options" role="group" aria-label="Sort order">
|
||||
<button class="library-sort-btn" data-sort="date">date</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">≥</span>
|
||||
<input type="number" id="filter-confidence" class="filter-number" min="0" max="100" placeholder="—" 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">≥</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">≥</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">≥</span>
|
||||
<input type="number" id="filter-score" class="filter-number" min="0" max="100" placeholder="—" 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">≥</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">≥</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">≥</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">≥</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>
|
||||
|
||||
<p class="library-empty is-filtered">No entries match the current filters.</p>
|
||||
|
||||
$if(research-entries)$
|
||||
<section class="library-section">
|
||||
<h2 id="research"><a href="/research/">Research</a></h2>
|
||||
<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">
|
||||
<a class="library-entry-title" href="$url$">$title$</a>
|
||||
<span class="library-entry-date">$date-created$</span>
|
||||
|
|
@ -29,7 +129,7 @@ $if(nonfiction-entries)$
|
|||
<section class="library-section">
|
||||
<h2 id="nonfiction"><a href="/nonfiction/">Nonfiction</a></h2>
|
||||
<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">
|
||||
<a class="library-entry-title" href="$url$">$title$</a>
|
||||
<span class="library-entry-date">$date-created$</span>
|
||||
|
|
@ -43,7 +143,7 @@ $if(fiction-entries)$
|
|||
<section class="library-section">
|
||||
<h2 id="fiction"><a href="/fiction/">Fiction</a></h2>
|
||||
<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">
|
||||
<a class="library-entry-title" href="$url$">$title$</a>
|
||||
<span class="library-entry-date">$date-created$</span>
|
||||
|
|
@ -57,7 +157,7 @@ $if(poetry-entries)$
|
|||
<section class="library-section">
|
||||
<h2 id="poetry"><a href="/poetry/">Poetry</a></h2>
|
||||
<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">
|
||||
<a class="library-entry-title" href="$url$">$title$</a>
|
||||
<span class="library-entry-date">$date-created$</span>
|
||||
|
|
@ -71,7 +171,7 @@ $if(music-entries)$
|
|||
<section class="library-section">
|
||||
<h2 id="music"><a href="/music/">Music</a></h2>
|
||||
<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">
|
||||
<a class="library-entry-title" href="$url$">$title$</a>
|
||||
<span class="library-entry-date">$date-created$</span>
|
||||
|
|
@ -85,7 +185,7 @@ $if(ai-entries)$
|
|||
<section class="library-section">
|
||||
<h2 id="ai"><a href="/ai/">AI</a></h2>
|
||||
<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">
|
||||
<a class="library-entry-title" href="$url$">$title$</a>
|
||||
<span class="library-entry-date">$date-created$</span>
|
||||
|
|
@ -99,7 +199,7 @@ $if(tech-entries)$
|
|||
<section class="library-section">
|
||||
<h2 id="tech"><a href="/tech/">Tech</a></h2>
|
||||
<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">
|
||||
<a class="library-entry-title" href="$url$">$title$</a>
|
||||
<span class="library-entry-date">$date-created$</span>
|
||||
|
|
@ -113,7 +213,7 @@ $if(miscellany-entries)$
|
|||
<section class="library-section">
|
||||
<h2 id="miscellany"><a href="/miscellany/">Miscellany</a></h2>
|
||||
<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">
|
||||
<a class="library-entry-title" href="$url$">$title$</a>
|
||||
<span class="library-entry-date">$date-created$</span>
|
||||
|
|
@ -126,60 +226,264 @@ $endif$
|
|||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
var STORAGE_KEY = 'library-sort';
|
||||
var DEFAULT = 'date';
|
||||
var MODES = { date: 1, title: 1, score: 1 };
|
||||
'use strict';
|
||||
|
||||
function titleOf(entry) {
|
||||
var el = entry.querySelector('.library-entry-title');
|
||||
var KEY = 'library-state';
|
||||
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() : '';
|
||||
}
|
||||
|
||||
function compare(a, b, mode) {
|
||||
if (mode === 'title') {
|
||||
return titleOf(a).localeCompare(titleOf(b));
|
||||
}
|
||||
if (mode === 'score') {
|
||||
// Entries without a score sink to the bottom; ties broken by date desc.
|
||||
var hasA = a.dataset.score !== undefined;
|
||||
var hasB = b.dataset.score !== undefined;
|
||||
if (hasA && !hasB) return -1;
|
||||
if (!hasA && hasB) return 1;
|
||||
if (hasA && hasB) {
|
||||
function compare(a, b) {
|
||||
var m = state.sort;
|
||||
if (m === 'title') return titleOf(a).localeCompare(titleOf(b));
|
||||
if (m === 'score') {
|
||||
var hA = a.dataset.score !== undefined;
|
||||
var hB = b.dataset.score !== undefined;
|
||||
if (hA && !hB) return -1;
|
||||
if (!hA && hB) return 1;
|
||||
if (hA && hB) {
|
||||
var diff = Number(b.dataset.score) - Number(a.dataset.score);
|
||||
if (diff !== 0) return diff;
|
||||
}
|
||||
// fall through to date-desc tiebreak
|
||||
}
|
||||
// date desc (ISO strings sort lexicographically)
|
||||
var da = a.dataset.date || '';
|
||||
var db = b.dataset.date || '';
|
||||
if (da < db) return 1;
|
||||
if (da > db) return -1;
|
||||
return 0;
|
||||
return da < db ? 1 : da > db ? -1 : 0;
|
||||
}
|
||||
|
||||
function applySort(mode) {
|
||||
if (!MODES[mode]) mode = DEFAULT;
|
||||
/* ---- Apply ---- */
|
||||
|
||||
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) {
|
||||
var entries = Array.prototype.slice.call(list.querySelectorAll('.library-entry'));
|
||||
entries.sort(function (a, b) { return compare(a, b, mode); });
|
||||
entries.forEach(function (el) { list.appendChild(el); });
|
||||
var entries = [].slice.call(list.querySelectorAll('.library-entry'));
|
||||
entries.sort(compare);
|
||||
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 () {
|
||||
var saved;
|
||||
try { saved = localStorage.getItem(STORAGE_KEY); } catch (e) {}
|
||||
applySort(saved || DEFAULT);
|
||||
load();
|
||||
apply();
|
||||
|
||||
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) {
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ $if(home)$<title>Levi Neuwirth</title>$else$<title>$title$ — Levi Neuwirth</ti
|
|||
<link rel="stylesheet" href="/css/images.css">
|
||||
$if(home)$<link rel="stylesheet" href="/css/home.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(memento-mori)$<link rel="stylesheet" href="/css/memento-mori.css">$endif$
|
||||
$if(catalog)$<link rel="stylesheet" href="/css/catalog.css">$endif$
|
||||
|
|
|
|||
|
|
@ -19,14 +19,14 @@
|
|||
$endif$
|
||||
$if(status)$
|
||||
<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$
|
||||
<span class="ep-status">$status$</span>
|
||||
$if(confidence)$<span class="ep-row" title="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(evidence-dots)$<span class="ep-row" title="Evidence quality: $evidence$/5"><span class="ep-dots">$evidence-dots$</span> evidence quality</span>$endif$
|
||||
$if(scope)$<span class="ep-row" title="Scope">$scope$ scope</span>$endif$
|
||||
$if(novelty)$<span class="ep-row" title="Novelty">$novelty$ novelty</span>$endif$
|
||||
$if(practicality)$<span class="ep-row" title="Practicality">$practicality$ practicality</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" data-ep-term="status">$status$</span>
|
||||
$if(confidence)$<span class="ep-row" data-ep-term="confidence">$confidence$% confidence</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" data-ep-term="evidence"><span class="ep-dots">$evidence-dots$</span> evidence quality</span>$endif$
|
||||
$if(scope)$<span class="ep-row" data-ep-term="scope">$scope$ scope</span>$endif$
|
||||
$if(novelty)$<span class="ep-row" data-ep-term="novelty">$novelty$ novelty</span>$endif$
|
||||
$if(practicality)$<span class="ep-row" data-ep-term="practicality">$practicality$ practicality</span>$endif$
|
||||
</div>
|
||||
$endif$
|
||||
<nav class="meta-row meta-pagelinks" aria-label="Page sections">
|
||||
|
|
|
|||
|
|
@ -36,9 +36,9 @@
|
|||
<div class="meta-footer-section meta-footer-epistemic" id="epistemic">
|
||||
<h3><a href="/colophon.html#living-documents">Epistemic</a></h3>
|
||||
<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(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>
|
||||
</div>
|
||||
$endif$
|
||||
|
|
|
|||
Loading…
Reference in New Issue