ontoref-outreach/site/htmx-assets/ext/loading-states.js
Jesús Pérez 72a389f056 site: materialize the outreach repo's site projection, with witnesses
First real commit of site/. Until now outreach/.git tracked exactly one file
(README.md) and every content pack stamped git_sha=5619a40 regardless of what it
shipped — a signature with no provenance behind it. The site had no witness, which
is why a deploy could erase two sessions of work with nothing to diff against and
nothing to restore from.

Tracked here (authored): site/content, site/config, site/i18n, site/public/images,
justfile, scripts/, .just/, run*.sh, rustelo.manifest.toml.

Ignored (reproducible, or secret):
  .k, .env                    real keys — outreach mirrors to a PUBLIC remote
  site/r, site/public/r       content indexes, rebuilt by `just content`
  provisioning/.content-packs 228 MB of deploy tarballs
  cache, data, logs_*.out     runtime droppings

Three trees are deliberately NOT ignored despite looking generated, because their
declared source cannot currently reproduce them — ignoring them would delete the
only copy:
  site/public/images          196 of 209 images exist nowhere else (assets/site is empty)
  site/content/{adr,catalog}  projections whose generators are unwired (gen-adr-pages)
  site/content/domains        or failing (`just graph`)
They become ignorable when an audit proves regeneration is a no-op, not before.

Fixes carried in this import
---------------------------
content: the mirror ran one way and reverted its own work. content_processor writes
site/r, but the recipe ended with `rsync -a site/public/r/ -> site/r/`, so each fresh
index was clobbered by the stale deploy-tree copy: the tool reported "index.json with
69 posts" while the disk kept 58. Meanwhile public/r is the tree the deploy pack ships,
so no new content could ever reach production. The two trees own different files, so
the mirror now runs both ways: content indexes r -> public/r (with --delete, or a
renamed slug survives as a live URL), and the four nu artifacts (about, adr-map,
taglines, content_graph) back public/r -> r, excluded from the outbound leg so stale
copies cannot overwrite the fresh originals.

templates: the assembler has two layers (framework defaults, source-project templates)
but the level chain has three (rustelo -> website-htmx-rustelo -> outreach/site). The
site level had no overlay slot, so its template work had nowhere legitimate to live and
ended up in htmx-templates/ — the one tree `just templates` rm -rf's. That is how the
nav submenus were lost. site/templates-overlay/ is the missing third slot; 214 lines
(nav submenus, subscribe CTA, post engagement block) now live in a declared source and
survive regeneration byte-identical.

adr: projected ADR-059..069 into the content tree. They were written months ago and
never reached the site because gen-adr-pages.nu is wired into no recipe — a generator
outside the dependency chain is a generator that does not run.

Gates added
-----------
templates-check  reassembles into a temp tree and asserts an empty diff against the
                 live one: any drift means htmx-templates/ holds work that exists
                 live one: any drift means htmx-templates/ holds work that exists
                 nowhere else and the next run deletes it.

Both gates run before the destructive operation, not after. A check you can only run
afterwards is not a gate, it is an autopsy.

Refs: ADR-062 (projection-not-own-repo: site is a Projection of outreach, not its own
repo — different deploy target is not a different visibility boundary), ADR-048
(materialization), ADR-066 (the check decides, never the reporter — which the content
pipeline, the surface that publishes ontoref, was the last place not to honour).
2026-07-11 23:44:00 +01:00

184 lines
5.4 KiB
JavaScript

;(function() {
const loadingStatesUndoQueue = []
function loadingStateContainer(target) {
return htmx.closest(target, '[data-loading-states]') || document.body
}
function mayProcessUndoCallback(target, callback) {
if (document.body.contains(target)) {
callback()
}
}
function mayProcessLoadingStateByPath(elt, requestPath) {
const pathElt = htmx.closest(elt, '[data-loading-path]')
if (!pathElt) {
return true
}
return pathElt.getAttribute('data-loading-path') === requestPath
}
function queueLoadingState(sourceElt, targetElt, doCallback, undoCallback) {
const delayElt = htmx.closest(sourceElt, '[data-loading-delay]')
if (delayElt) {
const delayInMilliseconds =
delayElt.getAttribute('data-loading-delay') || 200
const timeout = setTimeout(function() {
doCallback()
loadingStatesUndoQueue.push(function() {
mayProcessUndoCallback(targetElt, undoCallback)
})
}, delayInMilliseconds)
loadingStatesUndoQueue.push(function() {
mayProcessUndoCallback(targetElt, function() { clearTimeout(timeout) })
})
} else {
doCallback()
loadingStatesUndoQueue.push(function() {
mayProcessUndoCallback(targetElt, undoCallback)
})
}
}
function getLoadingStateElts(loadingScope, type, path) {
return Array.from(htmx.findAll(loadingScope, '[' + type + ']')).filter(
function(elt) { return mayProcessLoadingStateByPath(elt, path) }
)
}
function getLoadingTarget(elt) {
if (elt.getAttribute('data-loading-target')) {
return Array.from(
htmx.findAll(elt.getAttribute('data-loading-target'))
)
}
return [elt]
}
htmx.defineExtension('loading-states', {
onEvent: function(name, evt) {
if (name === 'htmx:beforeRequest') {
const container = loadingStateContainer(evt.target)
const loadingStateTypes = [
'data-loading',
'data-loading-class',
'data-loading-class-remove',
'data-loading-disable',
'data-loading-aria-busy'
]
const loadingStateEltsByType = {}
loadingStateTypes.forEach(function(type) {
loadingStateEltsByType[type] = getLoadingStateElts(
container,
type,
evt.detail.pathInfo.requestPath
)
})
loadingStateEltsByType['data-loading'].forEach(function(sourceElt) {
getLoadingTarget(sourceElt).forEach(function(targetElt) {
queueLoadingState(
sourceElt,
targetElt,
function() {
targetElt.style.display =
sourceElt.getAttribute('data-loading') ||
'inline-block'
},
function() { targetElt.style.display = 'none' }
)
})
})
loadingStateEltsByType['data-loading-class'].forEach(
function(sourceElt) {
const classNames = sourceElt
.getAttribute('data-loading-class')
.split(' ')
getLoadingTarget(sourceElt).forEach(function(targetElt) {
queueLoadingState(
sourceElt,
targetElt,
function() {
classNames.forEach(function(className) {
targetElt.classList.add(className)
})
},
function() {
classNames.forEach(function(className) {
targetElt.classList.remove(className)
})
}
)
})
}
)
loadingStateEltsByType['data-loading-class-remove'].forEach(
function(sourceElt) {
const classNames = sourceElt
.getAttribute('data-loading-class-remove')
.split(' ')
getLoadingTarget(sourceElt).forEach(function(targetElt) {
queueLoadingState(
sourceElt,
targetElt,
function() {
classNames.forEach(function(className) {
targetElt.classList.remove(className)
})
},
function() {
classNames.forEach(function(className) {
targetElt.classList.add(className)
})
}
)
})
}
)
loadingStateEltsByType['data-loading-disable'].forEach(
function(sourceElt) {
getLoadingTarget(sourceElt).forEach(function(targetElt) {
queueLoadingState(
sourceElt,
targetElt,
function() { targetElt.disabled = true },
function() { targetElt.disabled = false }
)
})
}
)
loadingStateEltsByType['data-loading-aria-busy'].forEach(
function(sourceElt) {
getLoadingTarget(sourceElt).forEach(function(targetElt) {
queueLoadingState(
sourceElt,
targetElt,
function() { targetElt.setAttribute('aria-busy', 'true') },
function() { targetElt.removeAttribute('aria-busy') }
)
})
}
)
}
if (name === 'htmx:beforeOnLoad') {
while (loadingStatesUndoQueue.length > 0) {
loadingStatesUndoQueue.shift()()
}
}
}
})
})()