A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
80
fork

Configure Feed

Select the types of activity you want to include in your feed.

update fonts

+211 -57
+2 -2
pkg/appview/handlers/home.go
··· 31 31 } 32 32 db.SetRegistryURL(featuredCards, h.RegistryURL) 33 33 34 - // Fetch recently updated repositories (top 24 by last push - 6 rows at 4-col xl) 35 - recentCards, err := db.GetRepoCards(h.ReadOnlyDB, 24, currentUserDID, db.SortByLastUpdate) 34 + // Fetch recently updated repositories (top 18 by last push - 6 rows at 3-col lg) 35 + recentCards, err := db.GetRepoCards(h.ReadOnlyDB, 18, currentUserDID, db.SortByLastUpdate) 36 36 if err != nil { 37 37 log.Printf("Error fetching recent repos: %v", err) 38 38 recentCards = []db.RepoCardData{}
+3 -1
pkg/appview/handlers/settings.go
··· 425 425 w.WriteHeader(http.StatusNoContent) 426 426 } 427 427 428 - // validOciClients is the set of allowed OCI client values 428 + // validOciClients is the set of allowed OCI client values. 429 + // "none" means "image reference only" — no `<client> pull ` prefix. 429 430 var validOciClients = map[string]bool{ 430 431 "docker": true, 431 432 "podman": true, 432 433 "buildah": true, 433 434 "nerdctl": true, 434 435 "crane": true, 436 + "none": true, 435 437 } 436 438 437 439 // UpdateOciClientHandler handles updating the preferred OCI client
pkg/appview/public/fonts/commit-mono-400-italic.woff2

This is a binary file and will not be displayed.

pkg/appview/public/fonts/commit-mono-400.woff2

This is a binary file and will not be displayed.

pkg/appview/public/fonts/commit-mono-700.woff2

This is a binary file and will not be displayed.

pkg/appview/public/fonts/figtree-latin-ext.woff2

This is a binary file and will not be displayed.

pkg/appview/public/fonts/figtree-latin.woff2

This is a binary file and will not be displayed.

pkg/appview/public/fonts/onest-latin-ext.woff2

This is a binary file and will not be displayed.

pkg/appview/public/fonts/onest-latin.woff2

This is a binary file and will not be displayed.

-1
pkg/appview/public/icons.svg
··· 43 43 <symbol id="loader" viewBox="0 0 24 24"><path d="M12 2v4"/><path d="m16.2 7.8 2.9-2.9"/><path d="M18 12h4"/><path d="m16.2 16.2 2.9 2.9"/><path d="M12 18v4"/><path d="m4.9 19.1 2.9-2.9"/><path d="M2 12h4"/><path d="m4.9 4.9 2.9 2.9"/></symbol> 44 44 <symbol id="loader-2" viewBox="0 0 24 24"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></symbol> 45 45 <symbol id="moon" viewBox="0 0 24 24"><path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"/></symbol> 46 - <symbol id="package" viewBox="0 0 24 24"><path d="M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z"/><path d="M12 22V12"/><polyline points="3.29 7 12 12 20.71 7"/><path d="m7.5 4.27 9 5.15"/></symbol> 47 46 <symbol id="pencil" viewBox="0 0 24 24"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/></symbol> 48 47 <symbol id="plus" viewBox="0 0 24 24"><path d="M5 12h14"/><path d="M12 5v14"/></symbol> 49 48 <symbol id="radio-tower" viewBox="0 0 24 24"><path d="M4.9 16.1C1 12.2 1 5.8 4.9 1.9"/><path d="M7.8 4.7a6.14 6.14 0 0 0-.8 7.5"/><circle cx="12" cy="9" r="2"/><path d="M16.2 4.8c2 2 2.26 5.11.8 7.47"/><path d="M19.1 1.9a9.96 9.96 0 0 1 0 14.1"/><path d="M9.5 18h5"/><path d="m8 22 4-11 4 11"/></symbol>
+1
pkg/appview/server.go
··· 505 505 506 506 mainRouter.Handle("/css/*", CacheMiddleware(http.StripPrefix("/css/", PublicSubdir("css", branding)), 31536000)) 507 507 mainRouter.Handle("/js/*", CacheMiddleware(http.StripPrefix("/js/", PublicSubdir("js", branding)), 31536000)) 508 + mainRouter.Handle("/fonts/*", CacheMiddleware(http.StripPrefix("/fonts/", PublicSubdir("fonts", branding)), 31536000)) 508 509 mainRouter.Handle("/static/*", CacheMiddleware(http.StripPrefix("/static/", PublicSubdir("static", branding)), 31536000)) 509 510 510 511 slog.Info("UI enabled", "home", "/", "settings", "/settings")
+116 -1
pkg/appview/src/css/main.css
··· 15 15 dark --prefersdark; 16 16 } 17 17 18 + /* ======================================== 19 + WEB FONTS — Onest (display), Figtree (body), Commit Mono (mono) 20 + All self-hosted under /fonts/. Variable bodies for display/body, 21 + static weights for mono. 22 + ======================================== */ 23 + 24 + /* Onest — variable (400..800), display face */ 25 + @font-face { 26 + font-family: "Onest"; 27 + font-style: normal; 28 + font-weight: 400 800; 29 + font-display: swap; 30 + src: url("/fonts/onest-latin.woff2") format("woff2"); 31 + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 32 + } 33 + @font-face { 34 + font-family: "Onest"; 35 + font-style: normal; 36 + font-weight: 400 800; 37 + font-display: swap; 38 + src: url("/fonts/onest-latin-ext.woff2") format("woff2"); 39 + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; 40 + } 41 + 42 + /* Figtree — variable (300..900), body face */ 43 + @font-face { 44 + font-family: "Figtree"; 45 + font-style: normal; 46 + font-weight: 300 900; 47 + font-display: swap; 48 + src: url("/fonts/figtree-latin.woff2") format("woff2"); 49 + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 50 + } 51 + @font-face { 52 + font-family: "Figtree"; 53 + font-style: normal; 54 + font-weight: 300 900; 55 + font-display: swap; 56 + src: url("/fonts/figtree-latin-ext.woff2") format("woff2"); 57 + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; 58 + } 59 + 60 + /* Commit Mono — static 400/400-italic/700, mono face */ 61 + @font-face { 62 + font-family: "Commit Mono"; 63 + font-style: normal; 64 + font-weight: 400; 65 + font-display: swap; 66 + src: url("/fonts/commit-mono-400.woff2") format("woff2"); 67 + } 68 + @font-face { 69 + font-family: "Commit Mono"; 70 + font-style: italic; 71 + font-weight: 400; 72 + font-display: swap; 73 + src: url("/fonts/commit-mono-400-italic.woff2") format("woff2"); 74 + } 75 + @font-face { 76 + font-family: "Commit Mono"; 77 + font-style: normal; 78 + font-weight: 700; 79 + font-display: swap; 80 + src: url("/fonts/commit-mono-700.woff2") format("woff2"); 81 + } 82 + 83 + /* ======================================== 84 + TYPE SYSTEM — semantic tokens 85 + Override Tailwind 4's default font stacks so `font-sans` / `font-mono` 86 + utilities (and DaisyUI components) pick up our vendored fonts. 87 + ======================================== */ 88 + @theme inline { 89 + --font-sans: "Figtree", ui-sans-serif, system-ui, sans-serif; 90 + --font-mono: "Commit Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; 91 + --font-display: "Onest", ui-sans-serif, system-ui, sans-serif; 92 + } 93 + 18 94 /* ============================================ 19 95 DARK - "Deep Ocean" 20 96 ============================================ */ ··· 120 196 border-color: var(--color-star); 121 197 } 122 198 199 + /* ======================================== 200 + TYPE UTILITIES — display font + tabular numerals 201 + `font-display` applies Onest; use it on brand lockups and hero 202 + headings. `tabular-nums` aligns numeric columns in dense tables. 203 + ======================================== */ 204 + @utility font-display { 205 + font-family: var(--font-display); 206 + letter-spacing: 0.005em; 207 + } 208 + @utility tabular-nums { 209 + font-variant-numeric: tabular-nums; 210 + } 211 + 123 212 [data-theme="dark"] { 124 213 --shadow-card-hover: 125 214 0 8px 25px oklch(67.1% 0.05 145 / 0.2), 0 4px 12px oklch(0% 0 0 / 0.2); ··· 161 250 } 162 251 163 252 /* ======================================== 164 - STICKY FOOTER LAYOUT 253 + STICKY FOOTER LAYOUT + BASE TYPE 165 254 ======================================== */ 166 255 @layer base { 167 256 body { 168 257 @apply min-h-screen flex flex-col; 258 + font-family: var(--font-sans); 259 + -webkit-font-smoothing: antialiased; 260 + text-rendering: optimizeLegibility; 169 261 } 170 262 171 263 main { 172 264 @apply flex-1; 173 265 } 266 + 267 + /* Cap long prose at a comfortable reading width. Pages that need to 268 + break out of this (dashboards, tables) override with max-w-* utilities. */ 269 + .prose { 270 + max-width: 68ch; 271 + } 272 + 273 + /* Every table gets tabular-nums by default. Registry UIs are tables of 274 + numbers (pull counts, sizes, CVE counts, timestamps) — digits should 275 + align vertically so the eye can compare rows. */ 276 + table { 277 + font-variant-numeric: tabular-nums; 278 + } 174 279 } 175 280 176 281 /* ======================================== ··· 243 348 white-space: nowrap; 244 349 border-width: 0; 245 350 } 351 + } 352 + 353 + /* ======================================== 354 + KEYBOARD FOCUS RING 355 + Applied only on :focus-visible so mouse users don't see it. 356 + Uses the primary hue so it reads as part of the Deep Ocean palette. 357 + ======================================== */ 358 + :where(a, button, [role="button"], [role="tab"], input, select, textarea, summary, [tabindex]):focus-visible { 359 + outline: 2px solid var(--color-primary); 360 + outline-offset: 2px; 246 361 } 247 362 248 363 /* ========================================
+1 -1
pkg/appview/templates/components/card-grid.html
··· 15 15 - .HasMore: bool - whether to show Load More button 16 16 */}} 17 17 {{ if .Repositories }} 18 - <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3{{ if eq .Columns 4 }} xl:grid-cols-4{{ end }} gap-4"> 18 + <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3{{ if eq .Columns 4 }} xl:grid-cols-4{{ end }} gap-6"> 19 19 {{ range .Repositories }} 20 20 {{ template "repo-card" . }} 21 21 {{ end }}
+4 -4
pkg/appview/templates/components/docker-command.html
··· 24 24 - Display: string - short form shown in the UI (e.g. "alice.bsky.social/myapp:v1.2.3") 25 25 - Copy: string - full command copied to clipboard (e.g. "docker pull atcr.io/alice.bsky.social/myapp:v1.2.3") 26 26 */}} 27 - <div class="cmd group !w-full"> 28 - {{ icon "package" "size-4 shrink-0 text-base-content/60" }} 29 - <code class="flex-1">{{ .Display }}</code> 30 - <button class="btn btn-ghost btn-xs shrink-0 sm:opacity-0 sm:group-hover:opacity-100 focus:opacity-100 transition-opacity" data-cmd="{{ .Copy }}" aria-label="Copy pull command to clipboard"> 27 + <div class="cmd group !w-full !bg-base-100"> 28 + {{ icon "terminal" "size-4 shrink-0 text-base-content/60" }} 29 + <code class="flex-1 min-w-0">{{ .Display }}</code> 30 + <button class="btn btn-ghost btn-xs absolute right-1 top-1/2 -translate-y-1/2 sm:opacity-0 sm:group-hover:opacity-100 focus:opacity-100 transition-opacity" data-cmd="{{ .Copy }}" aria-label="Copy pull command to clipboard"> 31 31 {{ icon "copy" "size-4" }} 32 32 </button> 33 33 </div>
+2
pkg/appview/templates/components/head.html
··· 16 16 17 17 <!-- Preload critical assets --> 18 18 <link rel="preload" href="/icons.svg" as="image" type="image/svg+xml"> 19 + <link rel="preload" href="/fonts/figtree-latin.woff2" as="font" type="font/woff2" crossorigin> 20 + <link rel="preload" href="/fonts/commit-mono-400.woff2" as="font" type="font/woff2" crossorigin> 19 21 20 22 <!-- Theme: apply early to prevent flash --> 21 23 <script>
+1 -1
pkg/appview/templates/components/hero.html
··· 4 4 */}} 5 5 <section class="hero bg-base-200 min-h-[60vh] py-16 pb-24 relative overflow-hidden"> 6 6 <div class="hero-content text-center flex-col relative z-10 w-full"> 7 - <h1 class="text-4xl md:text-5xl font-bold">your registry <span class="text-primary">at</span> sea.</h1> 7 + <h1 class="text-4xl md:text-5xl font-display font-bold tracking-tight">your registry <span class="text-primary">at</span> sea.</h1> 8 8 <p class="text-lg text-base-content/70 max-w-lg mt-4"> 9 9 Push and pull Docker images on the AT Protocol.<br> 10 10 Browse public registries or control your data.
+2 -2
pkg/appview/templates/components/nav-brand.html
··· 1 1 {{ define "nav-brand" }} 2 - <a href="/" class="flex items-center gap-2 text-2xl font-bold text-secondary no-underline"> 2 + <a href="/" class="flex items-center gap-2 text-2xl font-display font-bold text-secondary no-underline tracking-tight"> 3 3 <img src="/favicon-96x96.png" class="h-12 w-auto" alt="{{ .ClientName }} logo"> 4 - {{ .ClientName }} 4 + {{ .ClientName }} 5 5 </a> 6 6 {{ end }}
+5 -3
pkg/appview/templates/components/pull-command-switcher.html
··· 28 28 <option value="nerdctl"{{ if eq .OciClient "nerdctl" }} selected{{ end }}>nerdctl</option> 29 29 <option value="buildah"{{ if eq .OciClient "buildah" }} selected{{ end }}>buildah</option> 30 30 <option value="crane"{{ if eq .OciClient "crane" }} selected{{ end }}>crane</option> 31 + <option value="none"{{ if eq .OciClient "none" }} selected{{ end }}>image ref only</option> 31 32 </select> 32 33 <div id="pull-cmd-display" class="flex-1 min-w-0"> 33 34 {{ if .Tag }} 34 - {{ template "docker-command" (print (ociClientName .OciClient) " pull " .RegistryURL "/" .OwnerHandle "/" .RepoName ":" .Tag) }} 35 + {{ template "docker-command" (print (pullPrefix .OciClient) .RegistryURL "/" .OwnerHandle "/" .RepoName ":" .Tag) }} 35 36 {{ else }} 36 - {{ template "docker-command" (print (ociClientName .OciClient) " pull " .RegistryURL "/" .OwnerHandle "/" .RepoName ":latest") }} 37 + {{ template "docker-command" (print (pullPrefix .OciClient) .RegistryURL "/" .OwnerHandle "/" .RepoName ":latest") }} 37 38 {{ end }} 38 39 </div> 39 40 </div> ··· 59 60 } 60 61 61 62 window.updatePullCommand = function(client) { 62 - var cmd = client + ' pull ' + registryURL + '/' + ownerHandle + '/' + repoName + ':' + tag; 63 + var prefix = client === 'none' ? '' : client + ' pull '; 64 + var cmd = prefix + registryURL + '/' + ownerHandle + '/' + repoName + ':' + tag; 63 65 var container = document.getElementById('pull-cmd-display'); 64 66 if (!container) return; 65 67 var code = container.querySelector('code');
+10 -10
pkg/appview/templates/components/repo-card.html
··· 31 31 </div> 32 32 {{ end }} 33 33 <div class="flex-1 min-w-0"> 34 - <div class="font-semibold text-sm truncate"> 35 - <a href="/u/{{ .OwnerHandle }}" class="link link-primary" onclick="event.stopPropagation()">{{ .OwnerHandle }}</a> 36 - <span class="text-base-content/60">/</span> 37 - <a href="/r/{{ .OwnerHandle }}/{{ .Repository }}" class="link text-base-content hover:underline" onclick="event.stopPropagation()">{{ .Repository }}</a> 34 + <div class="font-semibold text-sm flex items-baseline gap-1 min-w-0"> 35 + <a href="/u/{{ .OwnerHandle }}" class="link link-primary truncate min-w-0" onclick="event.stopPropagation()">{{ .OwnerHandle }}</a> 36 + <span class="text-base-content/60 shrink-0">/</span> 37 + <a href="/r/{{ .OwnerHandle }}/{{ .Repository }}" class="link text-base-content hover:underline truncate min-w-0" onclick="event.stopPropagation()">{{ .Repository }}</a> 38 38 </div> 39 39 {{ if .Tag }} 40 40 <span class="block text-base-content/60 text-sm truncate">Tag: {{ .Tag }}</span> ··· 48 48 {{ if eq .ArtifactType "helm-chart" }} 49 49 {{ if .Tag }} 50 50 {{ template "image-ref" (dict 51 - "Display" (printf "%s/%s:%s" .OwnerHandle .Repository .Tag) 51 + "Display" (printf "%s/%s/%s:%s" .RegistryURL .OwnerHandle .Repository .Tag) 52 52 "Copy" (printf "helm pull oci://%s/%s/%s --version %s" .RegistryURL .OwnerHandle .Repository .Tag)) }} 53 53 {{ else }} 54 54 {{ template "image-ref" (dict 55 - "Display" (printf "%s/%s" .OwnerHandle .Repository) 55 + "Display" (printf "%s/%s/%s" .RegistryURL .OwnerHandle .Repository) 56 56 "Copy" (printf "helm pull oci://%s/%s/%s" .RegistryURL .OwnerHandle .Repository)) }} 57 57 {{ end }} 58 58 {{ else }} 59 59 {{ if .Tag }} 60 60 {{ template "image-ref" (dict 61 - "Display" (printf "%s/%s:%s" .OwnerHandle .Repository .Tag) 62 - "Copy" (printf "%s pull %s/%s/%s:%s" (ociClientName .OciClient) .RegistryURL .OwnerHandle .Repository .Tag)) }} 61 + "Display" (printf "%s/%s/%s:%s" .RegistryURL .OwnerHandle .Repository .Tag) 62 + "Copy" (printf "%s%s/%s/%s:%s" (pullPrefix .OciClient) .RegistryURL .OwnerHandle .Repository .Tag)) }} 63 63 {{ else }} 64 64 {{ template "image-ref" (dict 65 - "Display" (printf "%s/%s" .OwnerHandle .Repository) 66 - "Copy" (printf "%s pull %s/%s/%s" (ociClientName .OciClient) .RegistryURL .OwnerHandle .Repository)) }} 65 + "Display" (printf "%s/%s/%s" .RegistryURL .OwnerHandle .Repository) 66 + "Copy" (printf "%s%s/%s/%s" (pullPrefix .OciClient) .RegistryURL .OwnerHandle .Repository)) }} 67 67 {{ end }} 68 68 {{ end }} 69 69 </div>
+1 -1
pkg/appview/templates/pages/home.html
··· 45 45 {{ if .RecentRepos }} 46 46 <section> 47 47 <h2 class="text-2xl font-bold mb-6">What's New</h2> 48 - {{ template "card-grid" (dict "Repositories" .RecentRepos "Columns" 4) }} 48 + {{ template "card-grid" (dict "Repositories" .RecentRepos) }} 49 49 </section> 50 50 {{ end }} 51 51 </div>
+1 -1
pkg/appview/templates/pages/install.html
··· 9 9 {{ template "nav" . }} 10 10 11 11 <main class="container mx-auto px-4 py-8 max-w-4xl"> 12 - <h1 class="text-3xl font-bold mb-2">Install {{ .ClientShortName }} Credential Helper</h1> 12 + <h1 class="text-3xl font-display font-bold tracking-tight mb-2">Install {{ .ClientShortName }} Credential Helper</h1> 13 13 <p class="text-base-content/70 mb-8">The {{ .ClientShortName }} credential helper enables Docker to authenticate with {{ .ClientShortName }} registries using your ATProto identity.</p> 14 14 15 15 <div class="space-y-12">
+1 -1
pkg/appview/templates/pages/learn-more.html
··· 11 11 <main class="container mx-auto px-4 py-8 max-w-4xl"> 12 12 <!-- Hero Section --> 13 13 <section class="text-center mb-16"> 14 - <h1 class="text-4xl md:text-5xl font-bold mb-4">Docker meets the decentralized web</h1> 14 + <h1 class="text-4xl md:text-5xl font-display font-bold tracking-tight mb-4">Docker meets the decentralized web</h1> 15 15 <p class="text-xl text-base-content/70 max-w-2xl mx-auto"> 16 16 {{ .ClientName }} is an OCI-compliant container registry built on the AT Protocol. 17 17 Push and pull Docker images using your Bluesky identity.
+3 -2
pkg/appview/templates/pages/login.html
··· 10 10 11 11 <main class="min-h-[calc(100vh-4rem)] flex items-start justify-center px-4 pt-16 sm:pt-24"> 12 12 <div class="w-full max-w-2xl"> 13 - <h1 class="text-3xl sm:text-4xl font-semibold text-center mb-8">Sign in to {{ .ClientName }}</h1> 13 + <h1 class="text-3xl sm:text-4xl font-display font-semibold tracking-tight text-center mb-8">Sign in to {{ .ClientName }}</h1> 14 14 15 15 {{ if .Error }} 16 16 <div class="alert alert-error mb-6"> ··· 31 31 <input type="hidden" name="return_to" value="{{ .ReturnTo }}" /> 32 32 33 33 <div class="sailor-typeahead relative order-1"> 34 + <label for="handle" class="sr-only">Atmosphere handle or DID</label> 34 35 <input type="text" 35 36 id="handle" 36 37 name="handle" 37 38 class="input input-bordered input-lg w-full" 38 39 placeholder="alice.bsky.social" 39 - autocomplete="off" 40 + autocomplete="username" 40 41 autocapitalize="off" 41 42 autocorrect="off" 42 43 spellcheck="false"
+1 -1
pkg/appview/templates/pages/privacy.html
··· 9 9 {{ template "nav" . }} 10 10 11 11 <main class="container mx-auto px-4 py-8 max-w-4xl"> 12 - <h1 class="text-3xl font-bold mb-2">Privacy Policy - {{ .CompanyName }} ({{ .SiteURL }})</h1> 12 + <h1 class="text-3xl font-display font-bold tracking-tight mb-2">Privacy Policy - {{ .CompanyName }} ({{ .SiteURL }})</h1> 13 13 <p class="text-base-content/60 mb-8"><em>Last updated: January 2025</em></p> 14 14 15 15 <div class="prose prose-sm max-w-none space-y-8">
+3 -1
pkg/appview/templates/pages/repository.html
··· 15 15 <div class="flex gap-4 items-start"> 16 16 {{ template "repo-avatar" (dict "IconURL" .Repository.IconURL "RepositoryName" .Repository.Name "IsOwner" .IsOwner) }} 17 17 <div class="flex-1 min-w-0"> 18 - <h1 class="text-2xl font-bold"> 18 + <h1 class="text-2xl md:text-3xl font-display font-bold tracking-tight"> 19 19 <a href="/u/{{ .Owner.Handle }}" class="link link-primary">{{ .Owner.Handle }}</a> 20 20 <span class="text-base-content/60">/</span> 21 21 <span>{{ .Repository.Name }}</span> ··· 502 502 if (tab.dataset.tab === tabId) { 503 503 tab.classList.add('border-primary', 'text-primary'); 504 504 tab.classList.remove('border-transparent', 'text-base-content/60'); 505 + tab.setAttribute('aria-selected', 'true'); 505 506 } else { 506 507 tab.classList.remove('border-primary', 'text-primary'); 507 508 tab.classList.add('border-transparent', 'text-base-content/60'); 509 + tab.setAttribute('aria-selected', 'false'); 508 510 } 509 511 }); 510 512
+7 -6
pkg/appview/templates/pages/settings.html
··· 9 9 {{ template "nav" . }} 10 10 11 11 <main class="container mx-auto px-4 py-8"> 12 - <h1 class="text-3xl font-bold mb-6">Settings</h1> 12 + <h1 class="text-3xl font-display font-bold tracking-tight mb-6">Settings</h1> 13 13 14 14 <!-- Mobile identity info (below lg) --> 15 15 <div class="lg:hidden mb-4 space-y-1 text-xs text-base-content/50"> ··· 67 67 <p class="text-base-content/70 mt-1">Customize your experience across the site.</p> 68 68 </div> 69 69 70 - <!-- OCI Client Selector --> 70 + <!-- Preferred Client Selector --> 71 71 <div class="flex items-center gap-4"> 72 72 <div> 73 - <label class="text-sm font-medium">OCI Client</label> 74 - <p class="text-xs text-base-content/60">Changes how pull commands are displayed across the site.</p> 73 + <label for="oci-client-select" class="text-sm font-medium">Preferred client</label> 74 + <p id="oci-client-hint" class="text-xs text-base-content/60">Sets the pull command shown on repository pages. Choose <em>Image reference only</em> to copy just the image ref (useful for Kubernetes manifests or docker-compose).</p> 75 75 </div> 76 76 {{ $oci := .Profile.OciClient }} 77 - <select id="oci-client-select" class="select select-sm select-bordered min-w-40" 77 + <select id="oci-client-select" aria-describedby="oci-client-hint" class="select select-sm select-bordered min-w-40" 78 78 onchange="htmx.ajax('POST', '/api/profile/oci-client', {values: {oci_client: this.value}, swap: 'none'})"> 79 79 <option value="docker"{{ if or (eq $oci "") (eq $oci "docker") }} selected{{ end }}>Docker</option> 80 80 <option value="podman"{{ if eq $oci "podman" }} selected{{ end }}>Podman</option> 81 81 <option value="buildah"{{ if eq $oci "buildah" }} selected{{ end }}>Buildah</option> 82 82 <option value="nerdctl"{{ if eq $oci "nerdctl" }} selected{{ end }}>nerdctl</option> 83 83 <option value="crane"{{ if eq $oci "crane" }} selected{{ end }}>crane</option> 84 + <option value="none"{{ if eq $oci "none" }} selected{{ end }}>Image reference only</option> 84 85 </select> 85 86 </div> 86 87 ··· 189 190 }</code></pre> 190 191 </li> 191 192 <li>Run any Docker command: 192 - <div class="mt-2">{{ template "docker-command" (print (ociClientName .OciClient) " pull " .RegistryURL "/" .Profile.Handle "/myimage") }}</div> 193 + <div class="mt-2">{{ template "docker-command" (print (pullPrefix .OciClient) .RegistryURL "/" .Profile.Handle "/myimage") }}</div> 193 194 </li> 194 195 <li>Browser will open for authorization - click Approve</li> 195 196 <li>Done! Device is automatically authorized</li>
+1 -1
pkg/appview/templates/pages/terms.html
··· 9 9 {{ template "nav" . }} 10 10 11 11 <main class="container mx-auto px-4 py-8 max-w-4xl"> 12 - <h1 class="text-3xl font-bold mb-2">Terms of Service - {{ .CompanyName }} ({{ .SiteURL }})</h1> 12 + <h1 class="text-3xl font-display font-bold tracking-tight mb-2">Terms of Service - {{ .CompanyName }} ({{ .SiteURL }})</h1> 13 13 <p class="text-base-content/60 mb-8"><em>Last updated: January 2025</em></p> 14 14 15 15 <p class="mb-8">These Terms of Service ("Terms") govern your use of {{ .CompanyName }} ("{{ .SiteURL }}", "the Service", "we", "us", "our"). By using the Service, you agree to these Terms. If you do not agree, do not use the Service.</p>
+1 -1
pkg/appview/templates/pages/user.html
··· 32 32 </div> 33 33 {{ end }} 34 34 <div class="flex items-center gap-2"> 35 - <h1 class="text-2xl font-bold">{{ .ViewedUser.Handle }}</h1> 35 + <h1 class="text-2xl md:text-3xl font-display font-bold tracking-tight">{{ .ViewedUser.Handle }}</h1> 36 36 {{ if or (eq .SupporterBadge "Captain") (eq .SupporterBadge "owner") }} 37 37 <span class="badge badge-sm supporter-badge-owner">{{ .SupporterBadge }}</span> 38 38 {{ else if .SupporterBadge }}
+9 -3
pkg/appview/templates/partials/other_holds_table.html
··· 24 24 {{ else }}<span class="badge badge-xs badge-secondary">Crew</span>{{ end }} 25 25 </td> 26 26 <td class="text-center"> 27 - {{ if eq .Status "online" }}<span class="text-success" title="Online">&#9679;</span> 28 - {{ else if eq .Status "offline" }}<span class="text-error" title="Offline">&#9679;</span> 29 - {{ else }}<span class="text-base-content/50" title="Unknown" aria-label="Unknown status">&#9679;</span> 27 + {{ if eq .Status "online" }} 28 + <span class="text-success" aria-hidden="true">&#9679;</span> 29 + <span class="sr-only">Online</span> 30 + {{ else if eq .Status "offline" }} 31 + <span class="text-error" aria-hidden="true">&#9679;</span> 32 + <span class="sr-only">Offline</span> 33 + {{ else }} 34 + <span class="text-base-content/50" aria-hidden="true">&#9679;</span> 35 + <span class="sr-only">Unknown</span> 30 36 {{ end }} 31 37 </td> 32 38 <td class="text-right">
+17 -6
pkg/appview/templates/partials/repo-tag-section.html
··· 73 73 74 74 <!-- Tab Navigation --> 75 75 <div class="border-b border-base-300 mt-6"> 76 - <nav class="flex gap-0 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden" role="tablist"> 76 + <nav class="flex gap-0 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden" role="tablist" aria-label="Repository sections"> 77 77 <button class="repo-tab shrink-0 whitespace-nowrap px-4 sm:px-6 py-3 text-sm font-medium border-b-2 border-transparent text-base-content/60 transition-colors cursor-pointer" 78 78 data-tab="overview" 79 79 role="tab" 80 + aria-selected="false" 81 + aria-controls="tab-overview" 82 + id="overview-tab-btn" 80 83 onclick="switchRepoTab('overview')"> 81 84 Overview 82 85 </button> ··· 84 87 <button class="repo-tab shrink-0 whitespace-nowrap px-4 sm:px-6 py-3 text-sm font-medium border-b-2 border-transparent text-base-content/60 transition-colors cursor-pointer" 85 88 data-tab="layers" 86 89 role="tab" 90 + aria-selected="false" 91 + aria-controls="tab-layers" 87 92 id="layers-tab-btn" 88 93 onclick="switchRepoTab('layers')"> 89 94 Layers ··· 91 96 <button class="repo-tab shrink-0 whitespace-nowrap px-4 sm:px-6 py-3 text-sm font-medium border-b-2 border-transparent text-base-content/60 transition-colors cursor-pointer" 92 97 data-tab="vulns" 93 98 role="tab" 99 + aria-selected="false" 100 + aria-controls="tab-vulns" 94 101 id="vulns-tab-btn" 95 102 onclick="switchRepoTab('vulns')"> 96 103 Vulnerabilities ··· 98 105 <button class="repo-tab shrink-0 whitespace-nowrap px-4 sm:px-6 py-3 text-sm font-medium border-b-2 border-transparent text-base-content/60 transition-colors cursor-pointer" 99 106 data-tab="sbom" 100 107 role="tab" 108 + aria-selected="false" 109 + aria-controls="tab-sbom" 101 110 id="sbom-tab-btn" 102 111 onclick="switchRepoTab('sbom')"> 103 112 SBOM ··· 106 115 <button class="repo-tab shrink-0 whitespace-nowrap px-4 sm:px-6 py-3 text-sm font-medium border-b-2 border-transparent text-base-content/60 transition-colors cursor-pointer" 107 116 data-tab="artifacts" 108 117 role="tab" 118 + aria-selected="false" 119 + aria-controls="tab-artifacts" 109 120 id="artifacts-tab-btn" 110 121 onclick="switchRepoTab('artifacts')"> 111 122 Artifacts ··· 114 125 </div> 115 126 116 127 <!-- Tab Panels --> 117 - <div id="tab-overview" class="repo-panel"> 128 + <div id="tab-overview" class="repo-panel" role="tabpanel" aria-labelledby="overview-tab-btn" tabindex="0"> 118 129 <div id="overview-view" class="card bg-base-100 shadow-sm p-6 space-y-4 min-w-0"> 119 130 {{ if and .AIAdvisorEnabled .User .IsOwner .SelectedTag }} 120 131 <div id="ai-advisor-section"> ··· 169 180 </div> 170 181 171 182 {{ if .SelectedTag }} 172 - <div id="tab-layers" class="repo-panel hidden"> 183 + <div id="tab-layers" class="repo-panel hidden" role="tabpanel" aria-labelledby="layers-tab-btn" tabindex="0"> 173 184 <div id="layers-content"> 174 185 <div class="flex justify-center py-12"> 175 186 <span class="loading loading-spinner loading-lg"></span> ··· 177 188 </div> 178 189 </div> 179 190 180 - <div id="tab-vulns" class="repo-panel hidden"> 191 + <div id="tab-vulns" class="repo-panel hidden" role="tabpanel" aria-labelledby="vulns-tab-btn" tabindex="0"> 181 192 <div id="vulns-content"> 182 193 <div class="flex justify-center py-12"> 183 194 <span class="loading loading-spinner loading-lg"></span> ··· 185 196 </div> 186 197 </div> 187 198 188 - <div id="tab-sbom" class="repo-panel hidden"> 199 + <div id="tab-sbom" class="repo-panel hidden" role="tabpanel" aria-labelledby="sbom-tab-btn" tabindex="0"> 189 200 <div id="sbom-content"> 190 201 <div class="flex justify-center py-12"> 191 202 <span class="loading loading-spinner loading-lg"></span> ··· 194 205 </div> 195 206 {{ end }} 196 207 197 - <div id="tab-artifacts" class="repo-panel hidden"> 208 + <div id="tab-artifacts" class="repo-panel hidden" role="tabpanel" aria-labelledby="artifacts-tab-btn" tabindex="0"> 198 209 <div id="artifacts-content"> 199 210 <div class="flex justify-center py-12"> 200 211 <span class="loading loading-spinner loading-lg"></span>
+2 -2
pkg/appview/templates/partials/repo-tags.html
··· 56 56 {{ end }} 57 57 {{ else }} 58 58 {{ if .Entry.IsTagged }} 59 - {{ template "docker-command" (print (ociClientName .OciClient) " pull " .RegistryURL "/" .OwnerHandle "/" .RepoName ":" .Entry.Label) }} 59 + {{ template "docker-command" (print (pullPrefix .OciClient) .RegistryURL "/" .OwnerHandle "/" .RepoName ":" .Entry.Label) }} 60 60 {{ else }} 61 - {{ template "docker-command" (print (ociClientName .OciClient) " pull " .RegistryURL "/" .OwnerHandle "/" .RepoName "@" .Entry.Digest) }} 61 + {{ template "docker-command" (print (pullPrefix .OciClient) .RegistryURL "/" .OwnerHandle "/" .RepoName "@" .Entry.Digest) }} 62 62 {{ end }} 63 63 {{ end }} 64 64 </div>
+2 -2
pkg/appview/templates/partials/vuln-details.html
··· 44 44 45 45 {{ if .Matches }} 46 46 <!-- CVE table --> 47 - <div class="overflow-y-auto max-h-[32rem]"> 48 - <table class="table table-xs table-pin-rows w-full"> 47 + <div class="overflow-x-auto overflow-y-auto max-h-[32rem]"> 48 + <table class="table table-xs table-pin-rows w-full min-w-[40rem]"> 49 49 <thead> 50 50 <tr> 51 51 <th>CVE</th>
+13
pkg/appview/ui.go
··· 348 348 return client 349 349 }, 350 350 351 + // pullPrefix returns the "<client> pull " prefix for a pull command, or an 352 + // empty string when the user has selected "none" (image reference only). 353 + // Usage: {{ pullPrefix .OciClient }} 354 + "pullPrefix": func(client string) string { 355 + if client == "none" { 356 + return "" 357 + } 358 + if client == "" { 359 + return "docker pull " 360 + } 361 + return client + " pull " 362 + }, 363 + 351 364 // extraCSS returns a <style> block with consumer CSS overrides, or empty string. 352 365 "extraCSS": func() template.HTML { 353 366 if extraCSS == "" {
+2 -2
pkg/atproto/lexicon.go
··· 353 353 // overwrite) are deleted from PDS, and their layers are cleaned up by hold GC. 354 354 AutoRemoveUntagged bool `json:"autoRemoveUntagged,omitempty"` 355 355 356 - // OciClient is the preferred OCI client for pull commands (docker, podman, buildah, nerdctl, crane). 357 - // Defaults to "docker" if empty. 356 + // OciClient is the preferred client for pull commands (docker, podman, buildah, nerdctl, crane). 357 + // "none" means image reference only (no `<client> pull ` prefix). Defaults to "docker" if empty. 358 358 OciClient string `json:"ociClient,omitempty"` 359 359 360 360 // AIAdvisorEnabled controls whether the AI Image Advisor feature is active for this user.
-1
pkg/hold/admin/public/icons.svg
··· 43 43 <symbol id="loader" viewBox="0 0 24 24"><path d="M12 2v4"/><path d="m16.2 7.8 2.9-2.9"/><path d="M18 12h4"/><path d="m16.2 16.2 2.9 2.9"/><path d="M12 18v4"/><path d="m4.9 19.1 2.9-2.9"/><path d="M2 12h4"/><path d="m4.9 4.9 2.9 2.9"/></symbol> 44 44 <symbol id="loader-2" viewBox="0 0 24 24"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></symbol> 45 45 <symbol id="moon" viewBox="0 0 24 24"><path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"/></symbol> 46 - <symbol id="package" viewBox="0 0 24 24"><path d="M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z"/><path d="M12 22V12"/><polyline points="3.29 7 12 12 20.71 7"/><path d="m7.5 4.27 9 5.15"/></symbol> 47 46 <symbol id="pencil" viewBox="0 0 24 24"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/></symbol> 48 47 <symbol id="plus" viewBox="0 0 24 24"><path d="M5 12h14"/><path d="M12 5v14"/></symbol> 49 48 <symbol id="radio-tower" viewBox="0 0 24 24"><path d="M4.9 16.1C1 12.2 1 5.8 4.9 1.9"/><path d="M7.8 4.7a6.14 6.14 0 0 0-.8 7.5"/><circle cx="12" cy="9" r="2"/><path d="M16.2 4.8c2 2 2.26 5.11.8 7.47"/><path d="M19.1 1.9a9.96 9.96 0 0 1 0 14.1"/><path d="M9.5 18h5"/><path d="m8 22 4-11 4 11"/></symbol>