more accessiblity tweaks

This commit is contained in:
Evan Jarrett
2026-01-17 16:43:54 -06:00
parent ebb107ebec
commit 26f049fcbe
13 changed files with 68 additions and 25 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+13
View File
@@ -100,6 +100,19 @@
}
}
/* ========================================
ACCESSIBILITY UTILITIES
======================================== */
@layer utilities {
/* Screen reader only - visually hidden but accessible */
.sr-only {
@apply absolute w-px h-px p-0 -m-px overflow-hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
}
/* ========================================
CUSTOM COMPONENTS (Not in DaisyUI)
======================================== */
+27 -3
View File
@@ -104,10 +104,15 @@ document.addEventListener('DOMContentLoaded', () => {
});
// Copy to clipboard
function copyToClipboard(text) {
// text: the text to copy
// btn: optional button element for feedback (uses event.target if not provided)
function copyToClipboard(text, btn) {
if (!btn && typeof event !== 'undefined') {
btn = event.target.closest('button');
}
navigator.clipboard.writeText(text).then(() => {
if (!btn) return;
// Show success feedback
const btn = event.target.closest('button');
const originalHTML = btn.innerHTML;
btn.innerHTML = '<i data-lucide="check"></i> Copied!';
// Re-initialize Lucide icons for the new icon
@@ -126,6 +131,25 @@ function copyToClipboard(text) {
});
}
// Initialize copy buttons with data-cmd attribute and clickable cards with data-href
document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('click', (e) => {
// Handle copy buttons
const btn = e.target.closest('button[data-cmd]');
if (btn) {
copyToClipboard(btn.getAttribute('data-cmd'), btn);
return;
}
// Handle clickable cards (skip if clicking on interactive elements)
if (e.target.closest('a, button, input, .cmd')) return;
const card = e.target.closest('[data-href]');
if (card) {
window.location = card.getAttribute('data-href');
}
});
});
// Time ago helper (for client-side rendering)
function timeAgo(date) {
const seconds = Math.floor((new Date() - new Date(date)) / 1000);
@@ -465,7 +489,7 @@ class RecentAccountsHelper {
try {
const recent = localStorage.getItem('atcr_recent_handles');
return recent ? JSON.parse(recent) : [];
} catch {
} catch (_) {
return [];
}
}
@@ -5,10 +5,10 @@
Expects: string - the docker command to display
Usage: {{ template "docker-command" "docker pull atcr.io/alice/myapp:latest" }}
*/}}
<div class="cmd group" onclick="event.stopPropagation()">
<div class="cmd group">
<i data-lucide="terminal" class="size-4 shrink-0 text-base-content/60"></i>
<code>{{ . }}</code>
<button class="btn btn-ghost btn-xs absolute right-2 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity" onclick="event.stopPropagation(); copyToClipboard(this.getAttribute('data-cmd'))" data-cmd="{{ . }}" aria-label="Copy command to clipboard">
<button class="btn btn-ghost btn-xs absolute right-2 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity" data-cmd="{{ . }}" aria-label="Copy command to clipboard">
<i data-lucide="copy" class="size-4"></i>
</button>
</div>
+1 -1
View File
@@ -10,7 +10,7 @@
/amathea_manatee-576w.webp 576w,
/amathea_manatee-768w.webp 768w,
/amathea_manatee-1152w.webp 1152w"
sizes="(max-width: 640px) 384px, (max-width: 1024px) 448px, 576px"
sizes="(max-width: 767px) 384px, (max-width: 1023px) 448px, 576px"
type="image/webp">
<img src="/amathea_manatee.png"
width="1408" height="768"
@@ -3,8 +3,10 @@
<button onclick="toggleSearch()" class="btn btn-ghost btn-circle" aria-label="Search">
<i data-lucide="search" class="size-5"></i>
</button>
<form action="/search" method="get" class="nav-search-form">
<input type="text" id="nav-search-input" name="q" placeholder="Search images..." value="{{ .Query }}" class="input input-sm input-bordered" />
<form action="/search" method="get" class="nav-search-form" role="search">
<label for="nav-search-input" class="sr-only">Search images</label>
<input type="text" id="nav-search-input" name="q" placeholder="Search images..." value="{{ .Query }}" class="input input-sm input-bordered" aria-label="Search images" />
<button type="submit" class="sr-only">Search</button>
</form>
</div>
{{ end }}
@@ -17,7 +17,7 @@
- LastUpdated: time.Time (optional) - Last push time
- RegistryURL: string - Registry URL for docker commands (e.g., "atcr.io")
*/}}
<div class="card card-interactive bg-base-200 border-2 border-base-300 p-6 flex flex-col justify-between min-h-60 w-full" onclick="window.location='/r/{{ .OwnerHandle }}/{{ .Repository }}'">
<div class="card card-interactive bg-base-200 border-2 border-base-300 p-6 flex flex-col justify-between min-h-60 w-full" data-href="/r/{{ .OwnerHandle }}/{{ pathEncode .Repository }}">
<div class="flex gap-4 items-start">
{{ if .IconURL }}
<img src="{{ resizeImage .IconURL 96 }}" alt="{{ .Repository }}" loading="lazy" width="48" height="48" class="w-12 rounded-lg object-cover shrink-0">
@@ -34,7 +34,7 @@
<div class="font-semibold text-sm truncate">
<a href="/u/{{ .OwnerHandle }}" class="link link-primary" onclick="event.stopPropagation()">{{ .OwnerHandle }}</a>
<span class="text-base-content/60">/</span>
<a href="/r/{{ .OwnerHandle }}/{{ .Repository }}" class="link text-base-content hover:underline" onclick="event.stopPropagation()">{{ .Repository }}</a>
<a href="/r/{{ .OwnerHandle }}/{{ pathEncode .Repository }}" class="link text-base-content hover:underline" onclick="event.stopPropagation()">{{ .Repository }}</a>
</div>
{{ if .Tag }}
<span class="block text-base-content/60 text-sm truncate">Tag: {{ .Tag }}</span>
+2 -2
View File
@@ -11,9 +11,9 @@
<button class="btn btn-sm gap-2 btn-ghost group border border-transparent hover:border-primary{{ if .IsStarred }} border-amber-400!{{ end }}"
id="star-btn"
{{ if .IsStarred }}
hx-delete="/api/stars/{{ .Handle }}/{{ .Repository }}"
hx-delete="/api/stars/{{ .Handle }}/{{ pathEncode .Repository }}"
{{ else }}
hx-post="/api/stars/{{ .Handle }}/{{ .Repository }}"
hx-post="/api/stars/{{ .Handle }}/{{ pathEncode .Repository }}"
{{ end }}
hx-swap="outerHTML"
hx-on::before-request="this.disabled=true"
+1 -1
View File
@@ -38,7 +38,7 @@
</div>
<div class="collapse collapse-arrow bg-base-100 border border-base-300">
<input type="checkbox" />
<input type="checkbox" aria-label="Toggle technical details" />
<div class="collapse-title font-medium">
Technical Details
</div>
+7 -7
View File
@@ -4,21 +4,21 @@
<head>
<title>{{ if .Repository.Title }}{{ .Repository.Title }}{{ else }}{{ .Owner.Handle }}/{{ .Repository.Name }}{{ end }} - ATCR</title>
<meta name="description" content="{{ if .Repository.Description }}{{ .Repository.Description }}{{ else }}Container image {{ .Owner.Handle }}/{{ .Repository.Name }} on ATCR{{ end }}">
<link rel="canonical" href="https://{{ .RegistryURL }}/r/{{ .Owner.Handle }}/{{ .Repository.Name }}">
<link rel="canonical" href="https://{{ .RegistryURL }}/r/{{ .Owner.Handle }}/{{ pathEncode .Repository.Name }}">
<!-- Open Graph -->
<meta property="og:title" content="{{ .Owner.Handle }}/{{ .Repository.Name }} - ATCR">
<meta property="og:description" content="{{ if .Repository.Description }}{{ .Repository.Description }}{{ else }}Container image on ATCR{{ end }}">
<meta property="og:image" content="https://{{ .RegistryURL }}/og/r/{{ .Owner.Handle }}/{{ .Repository.Name }}">
<meta property="og:image" content="https://{{ .RegistryURL }}/og/r/{{ .Owner.Handle }}/{{ pathEncode .Repository.Name }}">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:type" content="website">
<meta property="og:url" content="https://{{ .RegistryURL }}/r/{{ .Owner.Handle }}/{{ .Repository.Name }}">
<meta property="og:url" content="https://{{ .RegistryURL }}/r/{{ .Owner.Handle }}/{{ pathEncode .Repository.Name }}">
<meta property="og:site_name" content="ATCR">
<!-- Twitter Card (used by Discord) -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ .Owner.Handle }}/{{ .Repository.Name }} - ATCR">
<meta name="twitter:description" content="{{ if .Repository.Description }}{{ .Repository.Description }}{{ else }}Container image on ATCR{{ end }}">
<meta name="twitter:image" content="https://{{ .RegistryURL }}/og/r/{{ .Owner.Handle }}/{{ .Repository.Name }}">
<meta name="twitter:image" content="https://{{ .RegistryURL }}/og/r/{{ .Owner.Handle }}/{{ pathEncode .Repository.Name }}">
<!-- JSON-LD Structured Data -->
<script type="application/ld+json">
{
@@ -26,7 +26,7 @@
"@type": "SoftwareSourceCode",
"name": "{{ .Owner.Handle }}/{{ .Repository.Name }}",
"description": {{ if .Repository.Description }}"{{ .Repository.Description }}"{{ else }}"Container image on ATCR"{{ end }},
"codeRepository": "https://{{ .RegistryURL }}/r/{{ .Owner.Handle }}/{{ .Repository.Name }}",
"codeRepository": "https://{{ .RegistryURL }}/r/{{ .Owner.Handle }}/{{ pathEncode .Repository.Name }}",
"author": {
"@type": "Person",
"name": "{{ .Owner.Handle }}",
@@ -170,7 +170,7 @@
</time>
{{ if $.IsOwner }}
<button class="btn btn-ghost btn-sm text-error"
hx-delete="/api/images/{{ $.Repository.Name }}/tags/{{ .Tag.Tag }}"
hx-delete="/api/images/{{ pathEncode $.Repository.Name }}/tags/{{ .Tag.Tag }}"
hx-confirm="Delete tag {{ .Tag.Tag }}?"
hx-target="#tag-{{ sanitizeID .Tag.Tag }}"
hx-swap="outerHTML"
@@ -256,7 +256,7 @@
</time>
{{ if $.IsOwner }}
<button class="btn btn-ghost btn-sm text-error"
onclick="deleteManifest('{{ $.Repository.Name }}', '{{ .Manifest.Digest }}', '{{ sanitizeID .Manifest.Digest }}')"
onclick="deleteManifest('{{ pathEncode $.Repository.Name }}', '{{ .Manifest.Digest }}', '{{ sanitizeID .Manifest.Digest }}')"
aria-label="Delete manifest {{ .Manifest.Digest | truncateDigest }}">
<i data-lucide="trash-2" class="size-4"></i>
</button>
+1 -1
View File
@@ -418,7 +418,7 @@
</ul>
<div class="space-y-2">
<p class="text-sm">Type <strong class="font-mono">DELETE {{ .Profile.Handle }}</strong> to confirm:</p>
<label for="confirm-delete-input" class="text-sm">Type <strong class="font-mono">DELETE {{ .Profile.Handle }}</strong> to confirm:</label>
<input type="text" id="confirm-delete-input" class="input input-bordered w-full font-mono" placeholder="DELETE {{ .Profile.Handle }}" autocomplete="off">
</div>
</div>
+4
View File
@@ -153,6 +153,10 @@ func Templates() (*template.Template, error) {
},
"assetHash": AssetHash,
"pathEncode": func(s string) string {
return url.PathEscape(s)
},
}
tmpl := template.New("").Funcs(funcMap)