mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-31 13:17:09 +00:00
265 lines
15 KiB
HTML
265 lines
15 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">
|
|
<a href="/u/{{ .Owner.Handle }}" class="link link-primary">{{ .Owner.Handle }}</a>
|
|
<span class="text-base-content/60">/</span>
|
|
<span>{{ .Repository.Name }}</span>
|
|
</h1>
|
|
{{ if .Repository.Description }}
|
|
<p class="text-base-content/70 mt-2">{{ .Repository.Description }}</p>
|
|
{{ end }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Metadata Row -->
|
|
<div class="flex flex-wrap items-center gap-3">
|
|
<div class="flex items-center gap-3">
|
|
{{ template "star" (dict "IsStarred" .IsStarred "StarCount" .Stats.StarCount "Interactive" true "Handle" .Owner.Handle "Repository" .Repository.Name) }}
|
|
{{ template "pull-count" (dict "PullCount" .Stats.PullCount) }}
|
|
{{ if .TagCount }}
|
|
<span class="flex items-center gap-1 text-sm text-base-content/70" title="{{ .TagCount }} tags">
|
|
{{ icon "tag" "size-4" }} {{ .TagCount }}
|
|
</span>
|
|
{{ end }}
|
|
{{ if .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" 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 text-base-content/60" }}
|
|
<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"
|
|
name="tag">
|
|
{{ range .AllTags }}
|
|
<option value="{{ . }}"{{ if eq . $.SelectedTag.Info.Tag.Tag }} selected{{ end }}>{{ . }}</option>
|
|
{{ end }}
|
|
</select>
|
|
{{ if gt (len .AllTags) 1 }}
|
|
<div class="dropdown dropdown-end" id="diff-dropdown">
|
|
<label tabindex="0" class="btn btn-ghost btn-sm gap-1" title="Compare with another tag">
|
|
{{ icon "git-compare" "size-4" }} Diff
|
|
</label>
|
|
<ul tabindex="0" class="dropdown-content menu bg-base-200 rounded-box z-10 w-56 p-2 shadow max-h-60 overflow-y-auto">
|
|
{{ range .AllTags }}
|
|
<li><a href="#" data-action="diff-to" data-diff-to="{{ . }}">{{ . }}</a></li>
|
|
{{ end }}
|
|
</ul>
|
|
</div>
|
|
{{ end }}
|
|
</div>
|
|
{{ if .SelectedTag.Info.IsMultiArch }}
|
|
<div id="platform-badges" class="flex flex-wrap items-center gap-1">
|
|
{{ range .SelectedTag.Info.Platforms }}
|
|
<span class="badge badge-sm badge-outline font-mono">{{ .OS }}/{{ .Architecture }}{{ if .Variant }}/{{ .Variant }}{{ end }}</span>
|
|
{{ end }}
|
|
</div>
|
|
{{ else }}
|
|
{{ $p := index .SelectedTag.Info.Platforms 0 }}
|
|
{{ if $p.OS }}
|
|
<div id="platform-badges">
|
|
<span class="badge badge-sm badge-outline font-mono">{{ $p.OS }}/{{ $p.Architecture }}{{ if $p.Variant }}/{{ $p.Variant }}{{ end }}</span>
|
|
</div>
|
|
{{ end }}
|
|
{{ end }}
|
|
{{ if .SelectedTag.Info.HasAttestations }}
|
|
<button class="badge badge-sm badge-soft badge-success cursor-pointer hover:opacity-80"
|
|
hx-get="/api/attestation-details?digest={{ .SelectedTag.Info.Digest | urlquery }}&did={{ .Owner.DID | urlquery }}&repo={{ .Repository.Name | urlquery }}"
|
|
hx-target="#attestation-modal-body"
|
|
hx-swap="innerHTML"
|
|
data-action="show-modal" data-modal-id="attestation-detail-modal">
|
|
{{ icon "shield-check" "size-3" }} Attested
|
|
</button>
|
|
{{ end }}
|
|
</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">
|
|
<button class="editor-tab px-4 py-2 text-sm font-medium border-b-2 border-primary text-primary"
|
|
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"
|
|
data-tab="preview" data-action="switch-editor-tab">
|
|
Preview
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- Write panel -->
|
|
<div id="editor-write" class="editor-panel">
|
|
<!-- 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">
|
|
{{ 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">
|
|
{{ 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">
|
|
{{ 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">
|
|
{{ 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">
|
|
{{ 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">
|
|
{{ 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">
|
|
{{ 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">
|
|
{{ 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">
|
|
<div id="preview-content" class="prose prose-sm max-w-none min-h-[20rem] 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>
|
|
|
|
<!-- 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">
|
|
<h3 id="manifest-delete-title" class="text-lg font-bold">Confirm Deletion</h3>
|
|
<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">
|
|
<h3 id="untagged-delete-title" class="text-lg font-bold">Delete Untagged Manifests</h3>
|
|
<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>
|
|
|
|
<!-- 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">
|
|
<h3 id="attestation-detail-title" class="text-lg font-bold">Attestation Details</h3>
|
|
<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 }}
|