lucide icon pack. clean up some templates/css

This commit is contained in:
Evan Jarrett
2025-10-29 23:21:28 -05:00
parent 2f2b8c8275
commit f07376c3d0
15 changed files with 386 additions and 96 deletions
+3
View File
@@ -70,6 +70,7 @@ type Push struct {
IconURL string
StarCount int
PullCount int
IsStarred bool // Whether the current user has starred this repository
CreatedAt time.Time
HoldEndpoint string // Hold endpoint for health checking
Reachable bool // Whether the hold endpoint is reachable
@@ -114,6 +115,7 @@ type FeaturedRepository struct {
IconURL string
StarCount int
PullCount int
IsStarred bool // Whether the current user has starred this repository
}
// RepositoryWithStats combines repository data with statistics
@@ -131,6 +133,7 @@ type RepoCardData struct {
IconURL string
StarCount int
PullCount int
IsStarred bool // Whether the current user has starred this repository
}
// PlatformInfo represents platform information (OS/Architecture)
+19 -10
View File
@@ -31,7 +31,7 @@ func escapeLikePattern(s string) string {
}
// GetRecentPushes fetches recent pushes with pagination
func GetRecentPushes(db *sql.DB, limit, offset int, userFilter string) ([]Push, int, error) {
func GetRecentPushes(db *sql.DB, limit, offset int, userFilter string, currentUserDID string) ([]Push, int, error) {
query := `
SELECT
u.did,
@@ -44,6 +44,7 @@ func GetRecentPushes(db *sql.DB, limit, offset int, userFilter string) ([]Push,
COALESCE((SELECT value FROM repository_annotations WHERE did = u.did AND repository = t.repository AND key = 'io.atcr.icon'), ''),
COALESCE(rs.pull_count, 0),
COALESCE((SELECT COUNT(*) FROM stars WHERE owner_did = u.did AND repository = t.repository), 0),
COALESCE((SELECT COUNT(*) FROM stars WHERE starrer_did = ? AND owner_did = u.did AND repository = t.repository), 0),
t.created_at,
m.hold_endpoint
FROM tags t
@@ -52,7 +53,7 @@ func GetRecentPushes(db *sql.DB, limit, offset int, userFilter string) ([]Push,
LEFT JOIN repository_stats rs ON t.did = rs.did AND t.repository = rs.repository
`
args := []any{}
args := []any{currentUserDID}
if userFilter != "" {
query += " WHERE u.handle = ? OR u.did = ?"
@@ -71,9 +72,11 @@ func GetRecentPushes(db *sql.DB, limit, offset int, userFilter string) ([]Push,
var pushes []Push
for rows.Next() {
var p Push
if err := rows.Scan(&p.DID, &p.Handle, &p.Repository, &p.Tag, &p.Digest, &p.Title, &p.Description, &p.IconURL, &p.PullCount, &p.StarCount, &p.CreatedAt, &p.HoldEndpoint); err != nil {
var isStarredInt int
if err := rows.Scan(&p.DID, &p.Handle, &p.Repository, &p.Tag, &p.Digest, &p.Title, &p.Description, &p.IconURL, &p.PullCount, &p.StarCount, &isStarredInt, &p.CreatedAt, &p.HoldEndpoint); err != nil {
return nil, 0, err
}
p.IsStarred = isStarredInt > 0
pushes = append(pushes, p)
}
@@ -95,7 +98,7 @@ func GetRecentPushes(db *sql.DB, limit, offset int, userFilter string) ([]Push,
}
// SearchPushes searches for pushes matching the query across handles, DIDs, repositories, and annotations
func SearchPushes(db *sql.DB, query string, limit, offset int) ([]Push, int, error) {
func SearchPushes(db *sql.DB, query string, limit, offset int, currentUserDID string) ([]Push, int, error) {
// Escape LIKE wildcards so they're treated literally
query = escapeLikePattern(query)
@@ -114,6 +117,7 @@ func SearchPushes(db *sql.DB, query string, limit, offset int) ([]Push, int, err
COALESCE((SELECT value FROM repository_annotations WHERE did = u.did AND repository = t.repository AND key = 'io.atcr.icon'), ''),
COALESCE(rs.pull_count, 0),
COALESCE((SELECT COUNT(*) FROM stars WHERE owner_did = u.did AND repository = t.repository), 0),
COALESCE((SELECT COUNT(*) FROM stars WHERE starrer_did = ? AND owner_did = u.did AND repository = t.repository), 0),
t.created_at,
m.hold_endpoint
FROM tags t
@@ -132,7 +136,7 @@ func SearchPushes(db *sql.DB, query string, limit, offset int) ([]Push, int, err
LIMIT ? OFFSET ?
`
rows, err := db.Query(sqlQuery, searchPattern, query, searchPattern, searchPattern, limit, offset)
rows, err := db.Query(sqlQuery, currentUserDID, searchPattern, query, searchPattern, searchPattern, limit, offset)
if err != nil {
return nil, 0, err
}
@@ -141,9 +145,11 @@ func SearchPushes(db *sql.DB, query string, limit, offset int) ([]Push, int, err
var pushes []Push
for rows.Next() {
var p Push
if err := rows.Scan(&p.DID, &p.Handle, &p.Repository, &p.Tag, &p.Digest, &p.Title, &p.Description, &p.IconURL, &p.PullCount, &p.StarCount, &p.CreatedAt, &p.HoldEndpoint); err != nil {
var isStarredInt int
if err := rows.Scan(&p.DID, &p.Handle, &p.Repository, &p.Tag, &p.Digest, &p.Title, &p.Description, &p.IconURL, &p.PullCount, &p.StarCount, &isStarredInt, &p.CreatedAt, &p.HoldEndpoint); err != nil {
return nil, 0, err
}
p.IsStarred = isStarredInt > 0
pushes = append(pushes, p)
}
@@ -1571,7 +1577,7 @@ func (m *MetricsDB) IncrementPushCount(did, repository string) error {
}
// GetFeaturedRepositories fetches top repositories sorted by stars and pulls
func GetFeaturedRepositories(db *sql.DB, limit int) ([]FeaturedRepository, error) {
func GetFeaturedRepositories(db *sql.DB, limit int, currentUserDID string) ([]FeaturedRepository, error) {
query := `
WITH latest_manifests AS (
SELECT did, repository, MAX(id) as latest_id
@@ -1596,7 +1602,8 @@ func GetFeaturedRepositories(db *sql.DB, limit int) ([]FeaturedRepository, error
COALESCE((SELECT value FROM repository_annotations WHERE did = m.did AND repository = m.repository AND key = 'org.opencontainers.image.description'), ''),
COALESCE((SELECT value FROM repository_annotations WHERE did = m.did AND repository = m.repository AND key = 'io.atcr.icon'), ''),
rs.pull_count,
rs.star_count
rs.star_count,
COALESCE((SELECT COUNT(*) FROM stars WHERE starrer_did = ? AND owner_did = m.did AND repository = m.repository), 0)
FROM latest_manifests lm
JOIN manifests m ON lm.latest_id = m.id
JOIN users u ON m.did = u.did
@@ -1605,7 +1612,7 @@ func GetFeaturedRepositories(db *sql.DB, limit int) ([]FeaturedRepository, error
LIMIT ?
`
rows, err := db.Query(query, limit)
rows, err := db.Query(query, currentUserDID, limit)
if err != nil {
return nil, err
}
@@ -1614,11 +1621,13 @@ func GetFeaturedRepositories(db *sql.DB, limit int) ([]FeaturedRepository, error
var featured []FeaturedRepository
for rows.Next() {
var f FeaturedRepository
var isStarredInt int
if err := rows.Scan(&f.OwnerDID, &f.OwnerHandle, &f.Repository,
&f.Title, &f.Description, &f.IconURL, &f.PullCount, &f.StarCount); err != nil {
&f.Title, &f.Description, &f.IconURL, &f.PullCount, &f.StarCount, &isStarredInt); err != nil {
return nil, err
}
f.IsStarred = isStarredInt > 0
featured = append(featured, f)
}
+16 -2
View File
@@ -11,6 +11,7 @@ import (
"atcr.io/pkg/appview/db"
"atcr.io/pkg/appview/holdhealth"
"atcr.io/pkg/appview/middleware"
)
// HomeHandler handles the home page
@@ -21,8 +22,14 @@ type HomeHandler struct {
}
func (h *HomeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Get current user DID (empty string if not logged in)
var currentUserDID string
if user := middleware.GetUser(r); user != nil {
currentUserDID = user.DID
}
// Fetch featured repositories (top 6)
featured, err := db.GetFeaturedRepositories(h.DB, 6)
featured, err := db.GetFeaturedRepositories(h.DB, 6, currentUserDID)
if err != nil {
// Log error but continue - featured section will be empty
featured = []db.FeaturedRepository{}
@@ -39,6 +46,7 @@ func (h *HomeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
IconURL: repo.IconURL,
StarCount: repo.StarCount,
PullCount: repo.PullCount,
IsStarred: repo.IsStarred,
}
}
@@ -77,7 +85,13 @@ func (h *RecentPushesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
userFilter = r.URL.Query().Get("q")
}
pushes, total, err := db.GetRecentPushes(h.DB, limit, offset, userFilter)
// Get current user DID (empty string if not logged in)
var currentUserDID string
if user := middleware.GetUser(r); user != nil {
currentUserDID = user.DID
}
pushes, total, err := db.GetRecentPushes(h.DB, limit, offset, userFilter, currentUserDID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
+8 -1
View File
@@ -8,6 +8,7 @@ import (
"strings"
"atcr.io/pkg/appview/db"
"atcr.io/pkg/appview/middleware"
)
// SearchHandler handles the search page
@@ -77,7 +78,13 @@ func (h *SearchResultsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
offset, _ = strconv.Atoi(o)
}
pushes, total, err := db.SearchPushes(h.DB, query, limit, offset)
// Get current user DID (empty string if not logged in)
var currentUserDID string
if user := middleware.GetUser(r); user != nil {
currentUserDID = user.DID
}
pushes, total, err := db.SearchPushes(h.DB, query, limit, offset, currentUserDID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
+1 -1
View File
@@ -129,5 +129,5 @@ func (h *UpdateDefaultHoldHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
}
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(`<div class="success"> Default hold updated successfully!</div>`))
w.Write([]byte(`<div class="success"><i data-lucide="check"></i> Default hold updated successfully!</div>`))
}
+226 -24
View File
@@ -24,9 +24,6 @@
/* Button text color */
--btn-text: #ffffff;
/* Theme toggle icon */
--theme-icon: '🌙';
/* Shadows */
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.05);
--shadow-md: 0 2px 4px rgba(0, 0, 0, 0.1);
@@ -61,7 +58,7 @@
--success: #34d399;
--success-bg: #064e3b;
--warning: #fbbf24;
--warning-bg: #78350f;
--warning-bg: #422006;
--danger: #dc3545;
--danger-bg: #7f1d1d;
--bg: #2a2a2a;
@@ -79,9 +76,6 @@
/* Button text color */
--btn-text: #ffffff;
/* Theme toggle icon */
--theme-icon: '☀️';
/* Shadows - lighter for dark backgrounds */
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
--shadow-md: 0 2px 4px rgba(0, 0, 0, 0.4);
@@ -347,23 +341,55 @@ button:hover, .btn:hover, .btn-primary:hover, .btn-secondary:hover {
text-decoration: none;
}
.theme-toggle-btn::before {
content: var(--theme-icon);
font-size: 1.2rem;
cursor: pointer;
.theme-toggle-btn {
display: inline-flex;
align-items: center;
justify-content: center;
}
.theme-toggle-btn .theme-icon {
width: 1.25rem;
height: 1.25rem;
}
.delete-btn {
background: var(--danger);
background: transparent;
border: none;
color: var(--danger);
padding: 0.25rem 0.5rem;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
}
.delete-btn:hover {
color: var(--danger);
}
.delete-btn:hover .lucide {
transform: scale(1.2);
}
.copy-btn {
padding: 0.25rem 0.75rem;
background: var(--button-primary);
color: var(--btn-text);
padding: 0.25rem 0.5rem;
background: transparent;
color: var(--secondary);
border: none;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
}
.copy-btn:hover {
color: var(--primary);
}
.copy-btn:hover .lucide {
transform: scale(1.2);
}
/* Cards */
@@ -414,6 +440,9 @@ button:hover, .btn:hover, .btn-primary:hover, .btn-secondary:hover {
}
.push-details {
display: flex;
align-items: center;
gap: 1rem;
color: var(--border-dark);
font-size: 0.9rem;
margin-bottom: 0.75rem;
@@ -441,6 +470,54 @@ button:hover, .btn:hover, .btn-primary:hover, .btn-secondary:hover {
gap: 0.5rem;
}
/* Docker command component */
.docker-command {
display: inline-flex;
position: relative;
align-items: center;
gap: 0.5rem;
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 0.75rem;
margin: 0.5rem 0;
max-width: 100%;
}
.docker-command-icon {
width: 1.25rem;
height: 1.25rem;
color: var(--secondary);
flex-shrink: 0;
}
.docker-command-text {
font-family: 'Monaco', 'Courier New', monospace;
font-size: 0.85rem;
color: var(--fg);
flex: 0 1 auto;
word-break: break-all;
}
.docker-command .copy-btn {
position: absolute;
right: 0.5rem;
top: 50%;
transform: translateY(-50%);
background: linear-gradient(to right, transparent, var(--code-bg) 30%);
padding: 0.5rem;
padding-left: 1.5rem;
border-radius: 4px;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
}
.docker-command:hover .copy-btn {
opacity: 1;
visibility: visible;
}
/* Digest tooltip on hover - using title attribute for native browser tooltip */
.digest {
cursor: default;
@@ -449,23 +526,41 @@ button:hover, .btn:hover, .btn-primary:hover, .btn-secondary:hover {
/* Digest copy button */
.digest-copy-btn {
background: transparent;
border: 1px solid var(--border);
border: none;
color: var(--secondary);
padding: 0.1rem 0.4rem;
font-size: 0.75rem;
border-radius: 3px;
cursor: pointer;
transition: all 0.2s;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
}
.digest-copy-btn:hover {
background: var(--hover-bg);
border-color: var(--primary);
color: var(--primary);
}
.digest-copy-btn:hover .lucide {
transform: scale(1.2);
}
.digest-copy-btn .lucide {
width: 0.875rem;
height: 0.875rem;
transition: transform 0.2s ease;
}
.delete-btn .lucide {
width: 1rem;
height: 1rem;
transition: transform 0.2s ease;
}
.copy-btn .lucide {
width: 1rem;
height: 1rem;
transition: transform 0.2s ease;
}
.separator {
color: var(--border);
}
@@ -538,11 +633,22 @@ button:hover, .btn:hover, .btn-primary:hover, .btn-secondary:hover {
.push-stat .star-icon {
color: var(--star);
font-size: 1rem;
width: 1rem;
height: 1rem;
stroke: var(--star);
fill: none;
}
.push-stat .star-icon.star-filled {
fill: var(--star);
}
.push-stat .pull-icon {
color: var(--primary);
font-size: 1rem;
width: 1rem;
height: 1rem;
stroke: var(--primary);
}
.push-stat .stat-count {
@@ -859,11 +965,36 @@ a.license-badge:hover {
margin: 1rem 0;
}
.note a {
color: var(--warning);
text-decoration: underline;
font-weight: 500;
}
.note a:hover {
color: var(--primary);
}
.note a:visited {
color: var(--warning);
}
.success {
background: var(--success-bg);
border-left: 4px solid var(--success);
padding: 1rem;
margin: 1rem 0;
display: flex;
align-items: center;
gap: 0.5rem;
}
.success .lucide {
width: 1.25rem;
height: 1.25rem;
color: var(--success);
stroke: var(--success);
flex-shrink: 0;
}
.error {
@@ -1075,11 +1206,30 @@ a.license-badge:hover {
background: var(--hover-bg);
}
/* Lucide icon base styles */
.lucide {
display: inline-block;
width: 1em;
height: 1em;
vertical-align: middle;
stroke-width: 2;
transition: transform 0.2s ease;
}
/* Star icon styles */
.star-icon {
font-size: 1.25rem;
line-height: 1;
transition: transform 0.2s ease;
color:var(--star);
color: var(--star);
width: 1.25rem;
height: 1.25rem;
stroke: var(--star);
fill: none;
}
.star-icon.star-filled {
fill: var(--star);
}
.star-btn:hover:not(:disabled) .star-icon {
@@ -1193,7 +1343,9 @@ a.license-badge:hover {
/* Offline manifest badge */
.offline-badge {
display: inline-block;
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.25rem 0.5rem;
background: var(--warning-bg);
color: var(--warning);
@@ -1204,9 +1356,16 @@ a.license-badge:hover {
margin-left: 0.5rem;
}
.offline-badge .lucide {
width: 0.875rem;
height: 0.875rem;
}
/* Checking manifest badge (health check in progress) */
.checking-badge {
display: inline-block;
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.25rem 0.5rem;
background: #e3f2fd;
color: #1976d2;
@@ -1217,6 +1376,11 @@ a.license-badge:hover {
margin-left: 0.5rem;
}
.checking-badge .lucide {
width: 0.875rem;
height: 0.875rem;
}
/* Hide offline manifests by default */
.manifest-item[data-reachable="false"] {
display: none;
@@ -1295,6 +1459,11 @@ a.license-badge:hover {
color: var(--secondary);
}
.manifest-type .lucide {
width: 0.95rem;
height: 0.95rem;
}
.platform-count {
color: var(--border-dark);
font-size: 0.85rem;
@@ -1431,11 +1600,22 @@ a.license-badge:hover {
.featured-stat .star-icon {
color: var(--star);
font-size: 1.1rem;
width: 1.1rem;
height: 1.1rem;
stroke: var(--star);
fill: none;
}
.featured-stat .star-icon.star-filled {
fill: var(--star);
}
.featured-stat .pull-icon {
color: var(--primary);
font-size: 1.1rem;
width: 1.1rem;
height: 1.1rem;
stroke: var(--primary);
}
.featured-stat .stat-count {
@@ -1599,6 +1779,14 @@ a.license-badge:hover {
line-height: 1;
}
.benefit-icon .lucide {
width: 3rem;
height: 3rem;
stroke-width: 1.5;
color: var(--primary);
stroke: var(--primary);
}
.benefit-card h3 {
font-size: 1.2rem;
margin-bottom: 0.75rem;
@@ -1634,6 +1822,20 @@ a.license-badge:hover {
font-size: 1.1rem;
}
.install-section a {
color: var(--primary);
text-decoration: underline;
font-weight: 500;
}
.install-section a:hover {
color: var(--primary-dark);
}
.install-section a:visited {
color: var(--primary);
}
.code-block {
background: var(--code-bg);
border: 1px solid var(--border);
+40 -11
View File
@@ -19,6 +19,19 @@ function updateThemeIcon() {
if (!themeBtn) return;
const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
const icon = themeBtn.querySelector('.theme-icon');
if (icon) {
// In dark mode, show sun icon (to switch to light)
// In light mode, show moon icon (to switch to dark)
icon.setAttribute('data-lucide', currentTheme === 'dark' ? 'sun' : 'moon');
// Re-initialize Lucide icons
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
themeBtn.setAttribute('aria-label', currentTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode');
}
@@ -26,11 +39,19 @@ function updateThemeIcon() {
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
// Show success feedback
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = ' Copied!';
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
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
setTimeout(() => {
btn.textContent = originalText;
btn.innerHTML = originalHTML;
// Re-initialize Lucide icons to restore original icon
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}, 2000);
}).catch(err => {
console.error('Failed to copy:', err);
@@ -75,7 +96,10 @@ function updateTimestamps() {
}
// Initial timestamp update
document.addEventListener('DOMContentLoaded', updateTimestamps);
document.addEventListener('DOMContentLoaded', () => {
updateTimestamps();
updateThemeIcon();
});
// Update timestamps after HTMX swaps
document.addEventListener('htmx:afterSwap', updateTimestamps);
@@ -90,10 +114,15 @@ function toggleRepo(name) {
if (details.style.display === 'none') {
details.style.display = 'block';
btn.textContent = '';
btn.innerHTML = '<i data-lucide="chevron-up"></i>';
} else {
details.style.display = 'none';
btn.textContent = '';
btn.innerHTML = '<i data-lucide="chevron-down"></i>';
}
// Re-initialize Lucide icons
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
@@ -154,7 +183,7 @@ async function toggleStar(handle, repository) {
try {
// Check current state
const isStarred = starIcon.textContent === '★';
const isStarred = starIcon.classList.contains('star-filled');
const method = isStarred ? 'DELETE' : 'POST';
const url = `/api/stars/${handle}/${repository}`;
@@ -180,13 +209,13 @@ async function toggleStar(handle, repository) {
// Update UI optimistically
if (data.starred) {
starIcon.textContent = '★';
starIcon.classList.add('star-filled');
starBtn.classList.add('starred');
// Optimistically increment count
const currentCount = parseInt(starCountEl.textContent) || 0;
starCountEl.textContent = currentCount + 1;
} else {
starIcon.textContent = '☆';
starIcon.classList.remove('star-filled');
starBtn.classList.remove('starred');
// Optimistically decrement count
const currentCount = parseInt(starCountEl.textContent) || 0;
@@ -229,7 +258,7 @@ async function loadStarStatus() {
const starData = await starResponse.json();
console.log('Star status data:', starData);
if (starData.starred) {
starIcon.textContent = '★';
starIcon.classList.add('star-filled');
starBtn.classList.add('starred');
}
} else {
@@ -0,0 +1,15 @@
{{ define "docker-command" }}
{{/*
Docker command component - displays a docker command with icon and copy button
Expects: string - the docker command to display
Usage: {{ template "docker-command" "docker pull atcr.io/alice/myapp:latest" }}
*/}}
<div class="docker-command">
<i data-lucide="terminal" class="docker-command-icon"></i>
<code class="docker-command-text">{{ . }}</code>
<button class="copy-btn" onclick="copyToClipboard(this.getAttribute('data-cmd'))" data-cmd="{{ . }}">
<i data-lucide="copy"></i>
</button>
</div>
{{ end }}
@@ -15,6 +15,20 @@
<!-- HTMX -->
<script src="https://unpkg.com/htmx.org@2.0.8/dist/htmx.min.js"></script>
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<!-- App Scripts -->
<script src="/js/app.js"></script>
<script>
// Initialize Lucide icons after DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
lucide.createIcons();
// Re-initialize icons after HTMX swaps content
document.body.addEventListener('htmx:afterSwap', () => {
lucide.createIcons();
});
});
</script>
{{ end }}
@@ -1,3 +1,5 @@
{{ define "nav-theme-toggle" }}
<button id="theme-toggle" onclick="toggleTheme()" class="btn-link theme-toggle-btn" aria-label="Toggle theme"></button>
<button id="theme-toggle" onclick="toggleTheme()" class="btn-link theme-toggle-btn" aria-label="Toggle theme">
<i data-lucide="moon" class="theme-icon"></i>
</button>
{{ end }}
@@ -31,11 +31,11 @@
</div>
<div class="featured-stats">
<span class="featured-stat">
<span class="star-icon"></span>
<i data-lucide="star" class="star-icon{{ if .IsStarred }} star-filled{{ end }}"></i>
<span class="stat-count">{{ .StarCount }}</span>
</span>
<span class="featured-stat">
<span class="pull-icon"></span>
<i data-lucide="arrow-down-to-line" class="pull-icon"></i>
<span class="stat-count">{{ .PullCount }}</span>
</span>
</div>
+3 -3
View File
@@ -39,17 +39,17 @@
<!-- Benefit Cards -->
<div class="hero-benefits">
<div class="benefit-card">
<div class="benefit-icon">🐳</div>
<div class="benefit-icon"><i data-lucide="ship"></i></div>
<h3>Works with Docker</h3>
<p>Use docker push & pull. No new tools to learn.</p>
</div>
<div class="benefit-card">
<div class="benefit-icon"></div>
<div class="benefit-icon"><i data-lucide="anchor"></i></div>
<h3>Your Data</h3>
<p>Join shared holds or captain your own storage.</p>
</div>
<div class="benefit-card">
<div class="benefit-icon">🧭</div>
<div class="benefit-icon"><i data-lucide="compass"></i></div>
<h3>Discover Images</h3>
<p>Browse and star public container registries.</p>
</div>
+12 -27
View File
@@ -34,7 +34,7 @@
<div class="repo-info-row">
<div class="repo-actions">
<button class="star-btn{{ if .IsStarred }} starred{{ end }}" id="star-btn" onclick="toggleStar('{{ .Owner.Handle }}', '{{ .Repository.Name }}')">
<span class="star-icon" id="star-icon">{{ if .IsStarred }}★{{ else }}☆{{ end }}</span>
<i data-lucide="star" class="star-icon{{ if .IsStarred }} star-filled{{ end }}" id="star-icon"></i>
<span class="star-count" id="star-count">{{ .StarCount }}</span>
</button>
</div>
@@ -81,19 +81,9 @@
<h3>Pull this image</h3>
{{ if .Tags }}
{{ $firstTag := index .Tags 0 }}
<div class="push-command">
<code class="pull-command">docker pull {{ $.RegistryURL }}/{{ $.Owner.Handle }}/{{ $.Repository.Name }}:{{ $firstTag.Tag.Tag }}</code>
<button class="copy-btn" onclick="copyToClipboard('docker pull {{ $.RegistryURL }}/{{ $.Owner.Handle }}/{{ $.Repository.Name }}:{{ $firstTag.Tag.Tag }}')">
Copy
</button>
</div>
{{ template "docker-command" (print "docker pull " $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name ":" $firstTag.Tag.Tag) }}
{{ else }}
<div class="push-command">
<code class="pull-command">docker pull {{ $.RegistryURL }}/{{ $.Owner.Handle }}/{{ $.Repository.Name }}:latest</code>
<button class="copy-btn" onclick="copyToClipboard('docker pull {{ $.RegistryURL }}/{{ $.Owner.Handle }}/{{ $.Repository.Name }}:latest')">
Copy
</button>
</div>
{{ template "docker-command" (print "docker pull " $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name ":latest") }}
{{ end }}
</div>
</div>
@@ -137,7 +127,7 @@
hx-confirm="Delete tag {{ .Tag.Tag }}?"
hx-target="#tag-{{ .Tag.Tag }}"
hx-swap="outerHTML">
🗑️
<i data-lucide="trash-2"></i>
</button>
{{ end }}
</div>
@@ -146,7 +136,7 @@
<div style="display: flex; justify-content: space-between; align-items: center;">
<div class="digest-container">
<code class="digest" title="{{ .Tag.Digest }}">{{ .Tag.Digest }}</code>
<button class="digest-copy-btn" onclick="copyToClipboard('{{ .Tag.Digest }}')">📋</button>
<button class="digest-copy-btn" onclick="copyToClipboard('{{ .Tag.Digest }}')"><i data-lucide="copy"></i></button>
</div>
{{ if .Platforms }}
<div class="platforms-inline">
@@ -157,12 +147,7 @@
{{ end }}
</div>
</div>
<div class="push-command">
<code class="pull-command">docker pull {{ $.RegistryURL }}/{{ $.Owner.Handle }}/{{ $.Repository.Name }}:{{ .Tag.Tag }}</code>
<button class="copy-btn" onclick="copyToClipboard('docker pull {{ $.RegistryURL }}/{{ $.Owner.Handle }}/{{ $.Repository.Name }}:{{ .Tag.Tag }}')">
Copy
</button>
</div>
{{ template "docker-command" (print "docker pull " $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name ":" .Tag.Tag) }}
</div>
{{ end }}
</div>
@@ -187,23 +172,23 @@
<div class="manifest-item-header">
<div>
{{ if .IsManifestList }}
<span class="manifest-type">📦 Multi-arch</span>
<span class="manifest-type"><i data-lucide="package"></i> Multi-arch</span>
{{ else }}
<span class="manifest-type">📄 Image</span>
<span class="manifest-type"><i data-lucide="file-text"></i> Image</span>
{{ end }}
{{ if .Pending }}
<span class="checking-badge"
hx-get="/api/manifest-health?endpoint={{ .Manifest.HoldEndpoint | urlquery }}"
hx-trigger="load delay:2s"
hx-swap="outerHTML">
🔄 Checking...
<i data-lucide="rotate-cw"></i> Checking...
</span>
{{ else if not .Reachable }}
<span class="offline-badge">⚠️ Offline</span>
<span class="offline-badge"><i data-lucide="alert-triangle"></i> Offline</span>
{{ end }}
<div class="digest-container">
<code class="digest manifest-digest" title="{{ .Manifest.Digest }}">{{ .Manifest.Digest }}</code>
<button class="digest-copy-btn" onclick="copyToClipboard('{{ .Manifest.Digest }}')">📋</button>
<button class="digest-copy-btn" onclick="copyToClipboard('{{ .Manifest.Digest }}')"><i data-lucide="copy"></i></button>
</div>
</div>
<div style="display: flex; gap: 1rem; align-items: center;">
@@ -213,7 +198,7 @@
{{ if $.IsOwner }}
<button class="delete-btn"
onclick="deleteManifest('{{ $.Repository.Name }}', '{{ .Manifest.Digest }}', '{{ sanitizeID .Manifest.Digest }}')">
🗑️
<i data-lucide="trash-2"></i>
</button>
{{ end }}
</div>
+21 -10
View File
@@ -65,8 +65,7 @@
<h3>First Time Setup</h3>
<ol>
<li>Install credential helper:
<pre><code>brew install atcr-credential-helper</code></pre>
(or download from releases)
<pre><code>curl -fsSL atcr.io/static/install.sh | bash</code></pre>
</li>
<li>Configure Docker to use the helper. Add to <code>~/.docker/config.json</code>:
<pre><code>{
@@ -76,7 +75,7 @@
}</code></pre>
</li>
<li>Run any Docker command:
<pre><code>docker pull {{ .RegistryURL }}/{{ .Profile.Handle }}/myimage</code></pre>
{{ template "docker-command" (print "docker pull " .RegistryURL "/" .Profile.Handle "/myimage") }}
</li>
<li>Browser will open for authorization - click Approve</li>
<li>Done! Device is automatically authorized</li>
@@ -205,7 +204,7 @@
.devices-section .setup-instructions {
margin: 1rem 0;
padding: 1.5rem;
background: #e3f2fd;
background: var(--code-bg);
border-radius: 4px;
}
.devices-section .setup-instructions h3 {
@@ -218,8 +217,9 @@
margin-bottom: 1rem;
}
.devices-section .setup-instructions pre {
background: #263238;
color: #aed581;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
padding: 0.75rem;
border-radius: 4px;
overflow-x: auto;
@@ -231,10 +231,21 @@
.devices-section .fallback-note {
margin-top: 1rem;
padding: 1rem;
background: #fff3cd;
border: 1px solid #ffc107;
background: var(--warning-bg);
border: 1px solid var(--warning);
border-radius: 4px;
}
.devices-section .fallback-note a {
color: var(--warning);
text-decoration: underline;
font-weight: 500;
}
.devices-section .fallback-note a:hover {
color: var(--primary);
}
.devices-section .fallback-note a:visited {
color: var(--warning);
}
.devices-section table {
width: 100%;
border-collapse: collapse;
@@ -244,10 +255,10 @@
.devices-section td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #ddd;
border-bottom: 1px solid var(--border);
}
.devices-section th {
background: #f5f5f5;
background: var(--code-bg);
font-weight: bold;
}
.devices-section .btn-danger {
@@ -17,11 +17,11 @@
</div>
<div class="push-stats">
<span class="push-stat">
<span class="star-icon"></span>
<i data-lucide="star" class="star-icon{{ if .IsStarred }} star-filled{{ end }}"></i>
<span class="stat-count">{{ .StarCount }}</span>
</span>
<span class="push-stat">
<span class="pull-icon"></span>
<i data-lucide="arrow-down-to-line" class="pull-icon"></i>
<span class="stat-count">{{ .PullCount }}</span>
</span>
</div>
@@ -35,9 +35,8 @@
<div class="push-details">
<div class="digest-container">
<code class="digest" title="{{ .Digest }}">{{ .Digest }}</code>
<button class="digest-copy-btn" onclick="copyToClipboard('{{ .Digest }}')">📋</button>
<button class="digest-copy-btn" onclick="copyToClipboard('{{ .Digest }}')"><i data-lucide="copy"></i></button>
</div>
<span class="separator"></span>
<time class="timestamp" datetime="{{ .CreatedAt.Format "2006-01-02T15:04:05Z07:00" }}">
{{ timeAgo .CreatedAt }}
</time>