mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-20 09:14:16 +00:00
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:
co-authored by
Claude Opus 5
parent
9d8bd513da
commit
7a0769d8e4
Vendored
+11
-11
File diff suppressed because one or more lines are too long
@@ -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', () => {
|
||||
|
||||
@@ -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)
|
||||
// ----------------------------------------
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user