mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-29 04:06:58 +00:00
more accessiblity tweaks
This commit is contained in:
File diff suppressed because one or more lines are too long
Vendored
+3
-3
File diff suppressed because one or more lines are too long
@@ -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)
|
||||
======================================== */
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user