mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-20 01:04:15 +00:00
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
269 lines
16 KiB
HTML
269 lines
16 KiB
HTML
{{ define "repository" }}
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
{{ template "head" . }}
|
|
{{ template "meta" .Meta }}
|
|
</head>
|
|
<body>
|
|
{{ template "nav" . }}
|
|
|
|
<main id="main-content" class="container mx-auto px-4 py-8">
|
|
<div class="space-y-6">
|
|
<!-- Static Header: Identity + Metadata (does not change with tag) -->
|
|
<div class="card bg-base-200 shadow-sm p-6 space-y-4 w-full">
|
|
<div class="flex gap-4 items-start">
|
|
{{ template "repo-avatar" (dict "IconURL" .Repository.IconURL "RepositoryName" .Repository.Name "IsOwner" .IsOwner) }}
|
|
<div class="flex-1 min-w-0">
|
|
<h1 class="text-2xl md:text-3xl font-display font-bold tracking-tight wrap-break-word min-w-0">
|
|
<a href="/u/{{ .Owner.Handle }}" class="link link-primary">{{ .Owner.Handle }}</a>
|
|
<span class="text-base-content/60" aria-hidden="true">/</span>
|
|
<span>{{ .Repository.Name }}</span>
|
|
</h1>
|
|
{{ if .Repository.Description }}
|
|
<p class="text-base-content/70 mt-2 line-clamp-3 wrap-break-word">{{ .Repository.Description }}</p>
|
|
{{ end }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Metadata Row -->
|
|
<div class="flex flex-wrap items-center gap-3">
|
|
<div class="flex items-center gap-3">
|
|
{{ if .StatsAvailable }}
|
|
{{ template "star" (dict "IsStarred" .IsStarred "StarCount" .Stats.StarCount "Interactive" true "Handle" .Owner.Handle "Repository" .Repository.Name "IsAuthenticated" (ne .User nil)) }}
|
|
{{ template "pull-count" (dict "PullCount" .Stats.PullCount) }}
|
|
{{ else }}
|
|
{{/* Stats query failed — show a subdued indicator rather
|
|
than zeros that could be mistaken for real counts. */}}
|
|
<span class="text-sm text-base-content/50" title="Star and pull counts are temporarily unavailable">
|
|
{{ icon "alert-circle" "size-4 inline" }} Stats unavailable
|
|
</span>
|
|
{{ end }}
|
|
{{ if .TagCount }}
|
|
<span class="flex items-center gap-1 text-sm text-base-content/70" title="{{ .TagCount }} {{ pluralize .TagCount "tag" "tags" }}">
|
|
{{ icon "tag" "size-4" }} {{ .TagCount }}
|
|
</span>
|
|
{{ end }}
|
|
{{ if and .StatsAvailable .Stats.LastPush }}
|
|
<span class="text-sm text-base-content/70" title="Last pushed {{ (derefTime .Stats.LastPush).Format "2006-01-02T15:04:05Z07:00" }}">
|
|
Updated {{ timeAgoShort (derefTime .Stats.LastPush) }}
|
|
</span>
|
|
{{ end }}
|
|
</div>
|
|
|
|
{{ if or .Repository.Licenses .Repository.SourceURL .Repository.DocumentationURL .Repository.Version }}
|
|
<div class="flex flex-wrap items-center gap-2 ml-auto">
|
|
{{ if .Repository.Version }}
|
|
<span class="badge badge-md badge-primary badge-outline" title="Version">
|
|
{{ .Repository.Version }}
|
|
</span>
|
|
{{ end }}
|
|
{{ if .Repository.Licenses }}
|
|
{{ range parseLicenses .Repository.Licenses }}
|
|
{{ if .IsValid }}
|
|
<a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" class="badge badge-md badge-soft badge-secondary" title="{{ .Name }}">
|
|
{{ .SPDXID }}
|
|
</a>
|
|
{{ else }}
|
|
<span class="badge badge-md badge-soft badge-secondary max-w-48 truncate" title="Custom license: {{ .Name }}">
|
|
{{ .Name }}
|
|
</span>
|
|
{{ end }}
|
|
{{ end }}
|
|
{{ end }}
|
|
{{ if .Repository.SourceURL }}
|
|
<a href="{{ .Repository.SourceURL }}" target="_blank" rel="noopener noreferrer" class="link link-primary text-sm flex items-center gap-1" aria-label="View source code (opens in new tab)">
|
|
{{ icon "external-link" "size-3" }} Source
|
|
</a>
|
|
{{ end }}
|
|
{{ if .Repository.DocumentationURL }}
|
|
<a href="{{ .Repository.DocumentationURL }}" target="_blank" rel="noopener noreferrer" class="link link-primary text-sm flex items-center gap-1" aria-label="View documentation (opens in new tab)">
|
|
{{ icon "book-open" "size-3" }} Docs
|
|
</a>
|
|
{{ end }}
|
|
</div>
|
|
{{ end }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tag Selector (stays in DOM, never swapped) -->
|
|
{{ if .SelectedTag }}
|
|
<div class="flex flex-wrap items-center gap-3">
|
|
<div class="flex items-center gap-2">
|
|
{{ 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 }}"
|
|
hx-target="#tag-content"
|
|
hx-swap="outerHTML"
|
|
hx-push-url="true"
|
|
hx-include="this"
|
|
hx-indicator="#tag-swap-spinner"
|
|
name="tag">
|
|
{{ range .AllTags }}
|
|
<option value="{{ . }}"{{ if eq . $.SelectedTag.Info.Tag.Tag }} selected{{ end }}>{{ . }}</option>
|
|
{{ end }}
|
|
</select>
|
|
<span id="tag-swap-spinner" class="htmx-indicator" aria-hidden="true">
|
|
{{ icon "loader" "size-4 animate-spin" }}
|
|
</span>
|
|
{{ if gt (len .AllTags) 1 }}
|
|
<div class="dropdown dropdown-end" id="diff-dropdown">
|
|
<button type="button" tabindex="0" class="btn btn-ghost btn-sm gap-1" title="Compare with another tag" aria-haspopup="menu" aria-expanded="false">
|
|
{{ icon "git-compare" "size-4" }} Diff
|
|
</button>
|
|
<ul tabindex="0" role="menu" class="dropdown-content menu bg-base-200 rounded-box z-10 w-auto min-w-56 max-w-md p-2 shadow max-h-60 overflow-y-auto">
|
|
{{ range .AllTags }}
|
|
<li role="none"><button type="button" role="menuitem" data-action="diff-to" data-diff-to="{{ . }}" class="truncate text-left">{{ . }}</button></li>
|
|
{{ end }}
|
|
</ul>
|
|
</div>
|
|
{{ end }}
|
|
</div>
|
|
{{/* Platform / attestation badges live INSIDE repo-tag-section
|
|
so they re-render with each tag swap. Putting them here
|
|
would freeze them at the page's initial-load tag. */}}
|
|
</div>
|
|
{{ end }}
|
|
|
|
<!-- Tag-Scoped Content (swapped via HTMX on tag change) -->
|
|
{{ template "repo-tag-section" . }}
|
|
|
|
<!-- Inline Editor (hidden, owner only) — outside HTMX-swapped section so edits survive tag changes -->
|
|
{{ if .IsOwner }}
|
|
<div id="overview-edit" class="card bg-base-200 shadow-sm p-6 hidden">
|
|
<!-- Write/Preview tabs -->
|
|
<div class="border-b border-base-300 mb-4">
|
|
<nav class="flex gap-0" role="tablist" aria-label="README editor mode">
|
|
<button class="editor-tab px-4 py-2 text-sm font-medium border-b-2 border-primary text-primary"
|
|
id="tab-write"
|
|
role="tab"
|
|
aria-selected="true"
|
|
aria-controls="editor-write"
|
|
data-tab="write" data-action="switch-editor-tab">
|
|
Write
|
|
</button>
|
|
<button class="editor-tab px-4 py-2 text-sm font-medium border-b-2 border-transparent text-base-content/60"
|
|
id="tab-preview"
|
|
role="tab"
|
|
aria-selected="false"
|
|
aria-controls="editor-preview"
|
|
tabindex="-1"
|
|
data-tab="preview" data-action="switch-editor-tab">
|
|
Preview
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- Write panel -->
|
|
<div id="editor-write" class="editor-panel" role="tabpanel" aria-labelledby="tab-write">
|
|
<!-- Toolbar -->
|
|
<div class="flex flex-wrap gap-1 mb-2 p-1 bg-base-200 rounded-lg">
|
|
<button type="button" class="btn btn-ghost btn-sm md:btn-xs" data-action="insert-md" data-md-type="heading" title="Heading" aria-label="Heading">
|
|
{{ icon "heading" "size-4" }}
|
|
</button>
|
|
<button type="button" class="btn btn-ghost btn-sm md:btn-xs" data-action="insert-md" data-md-type="bold" title="Bold" aria-label="Bold">
|
|
{{ icon "bold" "size-4" }}
|
|
</button>
|
|
<button type="button" class="btn btn-ghost btn-sm md:btn-xs" data-action="insert-md" data-md-type="italic" title="Italic" aria-label="Italic">
|
|
{{ icon "italic" "size-4" }}
|
|
</button>
|
|
<div class="divider divider-horizontal mx-0"></div>
|
|
<button type="button" class="btn btn-ghost btn-sm md:btn-xs" data-action="insert-md" data-md-type="link" title="Link" aria-label="Link">
|
|
{{ icon "link" "size-4" }}
|
|
</button>
|
|
<button type="button" class="btn btn-ghost btn-sm md:btn-xs" data-action="insert-md" data-md-type="image" title="Image" aria-label="Image">
|
|
{{ icon "image" "size-4" }}
|
|
</button>
|
|
<div class="divider divider-horizontal mx-0"></div>
|
|
<button type="button" class="btn btn-ghost btn-sm md:btn-xs" data-action="insert-md" data-md-type="ul" title="Bulleted list" aria-label="Bulleted list">
|
|
{{ icon "list" "size-4" }}
|
|
</button>
|
|
<button type="button" class="btn btn-ghost btn-sm md:btn-xs" data-action="insert-md" data-md-type="ol" title="Numbered list" aria-label="Numbered list">
|
|
{{ icon "list-ordered" "size-4" }}
|
|
</button>
|
|
<button type="button" class="btn btn-ghost btn-sm md:btn-xs" data-action="insert-md" data-md-type="code" title="Code" aria-label="Code">
|
|
{{ icon "code" "size-4" }}
|
|
</button>
|
|
</div>
|
|
<textarea id="md-editor"
|
|
class="textarea textarea-bordered w-full font-mono text-sm leading-relaxed"
|
|
rows="20"
|
|
maxlength="102400"
|
|
data-owner-did="{{ .Owner.DID }}"
|
|
data-repository="{{ .Repository.Name }}"
|
|
placeholder="Write your repository description in Markdown...">{{ .RawDescription }}</textarea>
|
|
</div>
|
|
|
|
<!-- Preview panel -->
|
|
<div id="editor-preview" class="editor-panel hidden" role="tabpanel" aria-labelledby="tab-preview">
|
|
<div id="preview-content" class="prose prose-sm max-w-none min-h-80 p-4 border border-base-300 rounded-lg">
|
|
<p class="text-base-content/60">Nothing to preview</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="flex justify-end gap-2 mt-4">
|
|
<button class="btn btn-sm btn-ghost" data-action="toggle-editor" data-show="false">Cancel</button>
|
|
<button class="btn btn-sm btn-primary" id="save-overview-btn" data-action="save-overview">Save</button>
|
|
</div>
|
|
</div>
|
|
{{ end }}
|
|
</div>
|
|
</main>
|
|
|
|
{{ if .IsOwner }}
|
|
<!-- Manifest Delete Confirmation Modal -->
|
|
<dialog id="manifest-delete-modal" class="modal" aria-modal="true" aria-labelledby="manifest-delete-title">
|
|
<div class="modal-box bg-base-200">
|
|
<h2 id="manifest-delete-title" class="text-lg font-bold">Confirm Deletion</h2>
|
|
<p id="manifest-delete-message" class="py-2">This manifest has associated tags that will also be deleted:</p>
|
|
<ul id="manifest-delete-tags" class="list-disc list-inside text-sm space-y-1"></ul>
|
|
<p class="font-bold py-2 text-error">This action cannot be undone.</p>
|
|
<div class="modal-action">
|
|
<button class="btn" data-action="close-manifest-delete-modal">Cancel</button>
|
|
<button class="btn btn-error" id="confirm-manifest-delete-btn">Delete All</button>
|
|
</div>
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop">
|
|
<button data-action="close-manifest-delete-modal">close</button>
|
|
</form>
|
|
</dialog>
|
|
|
|
<!-- Untagged Manifests Delete Confirmation Modal -->
|
|
<dialog id="untagged-delete-modal" class="modal" aria-modal="true" aria-labelledby="untagged-delete-title">
|
|
<div class="modal-box bg-base-200">
|
|
<h2 id="untagged-delete-title" class="text-lg font-bold">Delete Untagged Manifests</h2>
|
|
<p class="py-2">This will delete <strong>all</strong> untagged manifests in this repository, including those not currently visible.</p>
|
|
<p class="font-bold py-2 text-error">This action cannot be undone.</p>
|
|
<div class="modal-action">
|
|
<form method="dialog"><button class="btn">Cancel</button></form>
|
|
<button class="btn btn-error" id="confirm-untagged-delete-btn"
|
|
data-action="delete-untagged" data-repo="{{ .Repository.Name }}">
|
|
Delete All Untagged
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop"><button>close</button></form>
|
|
</dialog>
|
|
{{ end }}
|
|
|
|
<!-- Attestation Details Modal -->
|
|
<dialog id="attestation-detail-modal" class="modal" aria-modal="true" aria-labelledby="attestation-detail-title">
|
|
<div class="modal-box bg-base-200 max-w-2xl">
|
|
<h2 id="attestation-detail-title" class="text-lg font-bold">Attestation Details</h2>
|
|
<div id="attestation-modal-body" class="py-4">
|
|
<span class="loading loading-spinner loading-md"></span>
|
|
</div>
|
|
<div class="modal-action">
|
|
<form method="dialog"><button class="btn">Close</button></form>
|
|
</div>
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop"><button>close</button></form>
|
|
</dialog>
|
|
|
|
{{ template "footer" . }}
|
|
</body>
|
|
</html>
|
|
{{ end }}
|