appview: three small UI fixes, empty state, icon sizing, aria-selected

Artifacts filter had no empty state: filtering to zero matches left a blank
panel, indistinguishable from something being broken. It now uses the existing
state-empty partial, with no CTA since the filter input is right there. Rows
appended by Load More now also obey the active filter, which is a change to
existing behaviour but is required for the empty state to be truthful.

The tag icon beside the tag selector computed to 5.44px by 24px on every repo
page: the class was right, the flex parent was shrinking it. Adds shrink-0 to
that instance only. 31 other icons are direct flex children without shrink-0
and are left alone, since a site-wide sweep found only this one squeezed.

Digest-page scan tabs set role="tab" but never aria-selected, while the
repo-page tabs do. Both are now set server-side so first paint is correct, with
JS keeping them in sync afterwards, following switchRepoTab's pattern. The
diff-content tabs had the identical defect and are fixed too, since the handler
is generic over radio tabs and leaving them out would have meant attributes set
by JS but never by the server.

Includes the bundle rebuild for these and the two /auth/token-adjacent JS
changes, since nothing in the dev loop keeps that artifact current on its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PDqoCE1j3njokkZ9b1C5n9
This commit is contained in:
Evan Jarrett
2026-09-02 21:38:21 -05:00
co-authored by Claude Opus 5
parent 9d8bd513da
commit 7a0769d8e4
7 changed files with 73 additions and 17 deletions
+11 -11
View File
File diff suppressed because one or more lines are too long
+30
View File
@@ -682,6 +682,36 @@ document.addEventListener('change', (e) => {
helmCmdSwitcher_render(e.target.value);
});
// Digest page scan tabs (Vulnerabilities / SBOM). DaisyUI renders these as
// radio inputs, so the browser owns checked state and focus, but nothing owns
// aria-selected — screen readers were told every tab was unselected. Mirror
// the repo page's tab controller (repository.js switchRepoTab) and set the
// attribute across the whole radio group. Templates render the correct value
// for first paint; this only keeps it honest afterwards.
function syncRadioTablist(root) {
const scope = root && root.querySelectorAll ? root : document;
const groups = new Set();
scope.querySelectorAll('input[type="radio"][role="tab"][name]').forEach(el => groups.add(el.name));
groups.forEach(name => {
document.querySelectorAll('input[type="radio"][role="tab"]').forEach(el => {
if (el.name !== name) return;
el.setAttribute('aria-selected', el.checked ? 'true' : 'false');
});
});
}
// Delegated: covers the initial render and anything HTMX swaps in later
// (the digest page loads its content panel over the wire).
document.addEventListener('change', (e) => {
const t = e.target;
if (!t || t.type !== 'radio' || t.getAttribute('role') !== 'tab' || !t.name) return;
syncRadioTablist(t.form || document);
});
// Re-sync on paint and after swaps: a bfcache restore can re-check a
// different radio without ever firing a change event.
document.addEventListener('DOMContentLoaded', () => syncRadioTablist(document));
document.body.addEventListener('htmx:afterSettle', evt => syncRadioTablist(evt.detail && evt.detail.target));
// Install page: platform tab switcher. Each .platform-tab has data-platform
// pointing at a sibling panel (#<platform>-content). No-op off the install page.
document.addEventListener('DOMContentLoaded', () => {
+17 -1
View File
@@ -186,9 +186,16 @@ window.filterTags = function(query) {
filterTagsHandle = requestAnimationFrame(() => {
filterTagsHandle = 0;
const q = query.toLowerCase();
let visible = 0;
document.querySelectorAll('#tags-list .artifact-entry').forEach(el => {
el.style.display = (!q || el.dataset.tag.toLowerCase().includes(q)) ? '' : 'none';
const match = !q || el.dataset.tag.toLowerCase().includes(q);
el.style.display = match ? '' : 'none';
if (match) visible++;
});
// Without this the panel just goes blank and "no matches" is
// indistinguishable from a failed render.
const empty = document.getElementById('tags-filter-empty');
if (empty) empty.classList.toggle('hidden', visible > 0);
});
};
@@ -198,6 +205,15 @@ document.body.addEventListener('htmx:beforeSwap', () => {
if (filterTagsHandle) { cancelAnimationFrame(filterTagsHandle); filterTagsHandle = 0; }
});
// "Load More" appends entries into #tags-list without consulting the filter.
// Re-run it so the appended rows obey the active query and the empty state
// doesn't sit next to freshly-visible entries.
document.body.addEventListener('htmx:afterSettle', evt => {
if (!evt.detail.target || evt.detail.target.id !== 'tags-list') return;
const input = document.getElementById('tag-filter');
if (input && input.value) window.filterTags(input.value);
});
// ----------------------------------------
// Tag-scoped tab controller (reads config from #tag-content data attributes)
// ----------------------------------------
+1 -1
View File
@@ -90,7 +90,7 @@
{{ if .SelectedTag }}
<div class="flex flex-wrap items-center gap-3">
<div class="flex items-center gap-2">
{{ icon "tag" "size-6 text-base-content/60" }}
{{ icon "tag" "size-6 shrink-0 text-base-content/60" }}
<label for="tag-selector" class="sr-only">Select image tag</label>
<select id="tag-selector" class="select select-sm select-bordered font-mono"
hx-get="/r/{{ .Owner.Handle }}/{{ .Repository.Name }}"
@@ -51,7 +51,7 @@
<!-- Vulnerability + Package Diff (Right) -->
<div class="card bg-base-200 shadow-sm border border-base-300 p-6 min-w-0">
<div role="tablist" class="tabs tabs-bordered">
<input type="radio" id="diff-tab-vulns" name="diff-scan-tabs" role="tab" class="tab" aria-label="Vulnerabilities" aria-controls="diff-panel-vulns" checked="checked" />
<input type="radio" id="diff-tab-vulns" name="diff-scan-tabs" role="tab" class="tab" aria-label="Vulnerabilities" aria-controls="diff-panel-vulns" aria-selected="true" checked="checked" />
<div id="diff-panel-vulns" role="tabpanel" aria-labelledby="diff-tab-vulns" class="tab-content pt-4 space-y-4">
{{ if not .HasVulnData }}
@@ -202,7 +202,7 @@
{{ end }}
</div>
<input type="radio" id="diff-tab-sbom" name="diff-scan-tabs" role="tab" class="tab" aria-label="Packages" aria-controls="diff-panel-sbom" />
<input type="radio" id="diff-tab-sbom" name="diff-scan-tabs" role="tab" class="tab" aria-label="Packages" aria-controls="diff-panel-sbom" aria-selected="false" />
<div id="diff-panel-sbom" role="tabpanel" aria-labelledby="diff-tab-sbom" class="tab-content pt-4 space-y-4">
{{ if not .HasSbomData }}
@@ -48,7 +48,7 @@
<!-- Vulnerabilities + SBOM (Right) -->
<div class="card bg-base-200 shadow-sm border border-base-300 p-6 space-y-4 min-w-0">
<div role="tablist" class="tabs tabs-bordered">
<input type="radio" id="scan-tab-vulns" name="scan-tabs" role="tab" class="tab" aria-label="Vulnerabilities" aria-controls="scan-panel-vulns" checked="checked" />
<input type="radio" id="scan-tab-vulns" name="scan-tabs" role="tab" class="tab" aria-label="Vulnerabilities" aria-controls="scan-panel-vulns" aria-selected="true" checked="checked" />
<div id="scan-panel-vulns" role="tabpanel" aria-labelledby="scan-tab-vulns" class="tab-content pt-4">
{{ if .VulnData }}
{{ template "vuln-details" .VulnData }}
@@ -61,7 +61,7 @@
{{ end }}
</div>
<input type="radio" id="scan-tab-sbom" name="scan-tabs" role="tab" class="tab" aria-label="SBOM" aria-controls="scan-panel-sbom" />
<input type="radio" id="scan-tab-sbom" name="scan-tabs" role="tab" class="tab" aria-label="SBOM" aria-controls="scan-panel-sbom" aria-selected="false" />
<div id="scan-panel-sbom" role="tabpanel" aria-labelledby="scan-tab-sbom" class="tab-content pt-4">
{{ if .SbomData }}
{{ template "sbom-details" .SbomData }}
@@ -171,6 +171,16 @@
{{ template "artifact-entry-markup" (dict "Entry" . "OwnerDID" $.Owner.DID "OwnerHandle" $.Owner.Handle "RepoName" $.Repository.Name "RegistryURL" $.RegistryURL "OciClient" $.OciClient "IsOwner" $.IsOwner "ViewerDefaultHold" $.ViewerDefaultHold) }}
{{ end }}
</div>
{{/* Shown by filterTags() when the client-side filter hides every
entry. No CTA: the filter input is right above, so clearing it
is the obvious next move. */}}
<div id="tags-filter-empty" class="hidden">
{{ template "state-empty" (dict
"Icon" "search"
"Title" "No artifacts match that filter"
"Subtext" "Clear the filter box above to see all artifacts again."
) }}
</div>
{{ template "load-more-button" . }}
</div>