From f07376c3d0f7dcdef84cae9f47683ddf3a94cbcf Mon Sep 17 00:00:00 2001 From: Evan Jarrett Date: Wed, 29 Oct 2025 23:21:28 -0500 Subject: [PATCH] lucide icon pack. clean up some templates/css --- pkg/appview/db/models.go | 3 + pkg/appview/db/queries.go | 29 +- pkg/appview/handlers/home.go | 18 +- pkg/appview/handlers/search.go | 9 +- pkg/appview/handlers/settings.go | 2 +- pkg/appview/static/css/style.css | 250 ++++++++++++++++-- pkg/appview/static/js/app.js | 51 +++- .../templates/components/docker-command.html | 15 ++ pkg/appview/templates/components/head.html | 14 + .../components/nav-theme-toggle.html | 4 +- .../templates/components/repo-card.html | 4 +- pkg/appview/templates/pages/home.html | 6 +- pkg/appview/templates/pages/repository.html | 39 +-- pkg/appview/templates/pages/settings.html | 31 ++- pkg/appview/templates/partials/push-list.html | 7 +- 15 files changed, 386 insertions(+), 96 deletions(-) create mode 100644 pkg/appview/templates/components/docker-command.html diff --git a/pkg/appview/db/models.go b/pkg/appview/db/models.go index 175a449..e5405ee 100644 --- a/pkg/appview/db/models.go +++ b/pkg/appview/db/models.go @@ -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) diff --git a/pkg/appview/db/queries.go b/pkg/appview/db/queries.go index ef63217..3bededc 100644 --- a/pkg/appview/db/queries.go +++ b/pkg/appview/db/queries.go @@ -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) } diff --git a/pkg/appview/handlers/home.go b/pkg/appview/handlers/home.go index 9394106..6feb6ab 100644 --- a/pkg/appview/handlers/home.go +++ b/pkg/appview/handlers/home.go @@ -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 diff --git a/pkg/appview/handlers/search.go b/pkg/appview/handlers/search.go index 06f37b9..d393e52 100644 --- a/pkg/appview/handlers/search.go +++ b/pkg/appview/handlers/search.go @@ -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 diff --git a/pkg/appview/handlers/settings.go b/pkg/appview/handlers/settings.go index c7d9e48..5430281 100644 --- a/pkg/appview/handlers/settings.go +++ b/pkg/appview/handlers/settings.go @@ -129,5 +129,5 @@ func (h *UpdateDefaultHoldHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ } w.Header().Set("Content-Type", "text/html") - w.Write([]byte(`
✓ Default hold updated successfully!
`)) + w.Write([]byte(`
Default hold updated successfully!
`)) } diff --git a/pkg/appview/static/css/style.css b/pkg/appview/static/css/style.css index 1875a87..60c68ea 100644 --- a/pkg/appview/static/css/style.css +++ b/pkg/appview/static/css/style.css @@ -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); diff --git a/pkg/appview/static/js/app.js b/pkg/appview/static/js/app.js index d95ecfe..fdebac1 100644 --- a/pkg/appview/static/js/app.js +++ b/pkg/appview/static/js/app.js @@ -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 = ' 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 = ''; } else { details.style.display = 'none'; - btn.textContent = '▼'; + btn.innerHTML = ''; + } + + // 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 { diff --git a/pkg/appview/templates/components/docker-command.html b/pkg/appview/templates/components/docker-command.html new file mode 100644 index 0000000..38511db --- /dev/null +++ b/pkg/appview/templates/components/docker-command.html @@ -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" }} +*/}} +
+ + {{ . }} + +
+{{ end }} diff --git a/pkg/appview/templates/components/head.html b/pkg/appview/templates/components/head.html index dc1bfa4..2107bf0 100644 --- a/pkg/appview/templates/components/head.html +++ b/pkg/appview/templates/components/head.html @@ -15,6 +15,20 @@ + + + + {{ end }} diff --git a/pkg/appview/templates/components/nav-theme-toggle.html b/pkg/appview/templates/components/nav-theme-toggle.html index 64d47d6..cdb74c0 100644 --- a/pkg/appview/templates/components/nav-theme-toggle.html +++ b/pkg/appview/templates/components/nav-theme-toggle.html @@ -1,3 +1,5 @@ {{ define "nav-theme-toggle" }} - + {{ end }} diff --git a/pkg/appview/templates/components/repo-card.html b/pkg/appview/templates/components/repo-card.html index f76ae21..dc66a95 100644 --- a/pkg/appview/templates/components/repo-card.html +++ b/pkg/appview/templates/components/repo-card.html @@ -31,11 +31,11 @@ diff --git a/pkg/appview/templates/pages/home.html b/pkg/appview/templates/pages/home.html index 2ba0e61..ca49e2a 100644 --- a/pkg/appview/templates/pages/home.html +++ b/pkg/appview/templates/pages/home.html @@ -39,17 +39,17 @@
-
🐳
+

Works with Docker

Use docker push & pull. No new tools to learn.

-
+

Your Data

Join shared holds or captain your own storage.

-
🧭
+

Discover Images

Browse and star public container registries.

diff --git a/pkg/appview/templates/pages/repository.html b/pkg/appview/templates/pages/repository.html index 58c5e24..5c7a156 100644 --- a/pkg/appview/templates/pages/repository.html +++ b/pkg/appview/templates/pages/repository.html @@ -34,7 +34,7 @@
@@ -81,19 +81,9 @@

Pull this image

{{ if .Tags }} {{ $firstTag := index .Tags 0 }} -
- docker pull {{ $.RegistryURL }}/{{ $.Owner.Handle }}/{{ $.Repository.Name }}:{{ $firstTag.Tag.Tag }} - -
+ {{ template "docker-command" (print "docker pull " $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name ":" $firstTag.Tag.Tag) }} {{ else }} -
- docker pull {{ $.RegistryURL }}/{{ $.Owner.Handle }}/{{ $.Repository.Name }}:latest - -
+ {{ template "docker-command" (print "docker pull " $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name ":latest") }} {{ end }}
@@ -137,7 +127,7 @@ hx-confirm="Delete tag {{ .Tag.Tag }}?" hx-target="#tag-{{ .Tag.Tag }}" hx-swap="outerHTML"> - 🗑️ + {{ end }} @@ -146,7 +136,7 @@
{{ .Tag.Digest }} - +
{{ if .Platforms }}
@@ -157,12 +147,7 @@ {{ end }}
-
- docker pull {{ $.RegistryURL }}/{{ $.Owner.Handle }}/{{ $.Repository.Name }}:{{ .Tag.Tag }} - -
+ {{ template "docker-command" (print "docker pull " $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name ":" .Tag.Tag) }} {{ end }} @@ -187,23 +172,23 @@
{{ if .IsManifestList }} - 📦 Multi-arch + Multi-arch {{ else }} - 📄 Image + Image {{ end }} {{ if .Pending }} - 🔄 Checking... + Checking... {{ else if not .Reachable }} - ⚠️ Offline + Offline {{ end }}
{{ .Manifest.Digest }} - +
@@ -213,7 +198,7 @@ {{ if $.IsOwner }} {{ end }}
diff --git a/pkg/appview/templates/pages/settings.html b/pkg/appview/templates/pages/settings.html index a4d1df5..dab2b46 100644 --- a/pkg/appview/templates/pages/settings.html +++ b/pkg/appview/templates/pages/settings.html @@ -65,8 +65,7 @@

First Time Setup

  1. Install credential helper: -
    brew install atcr-credential-helper
    - (or download from releases) +
    curl -fsSL atcr.io/static/install.sh | bash
  2. Configure Docker to use the helper. Add to ~/.docker/config.json:
    {
    @@ -76,7 +75,7 @@
     }
  3. Run any Docker command: -
    docker pull {{ .RegistryURL }}/{{ .Profile.Handle }}/myimage
    + {{ template "docker-command" (print "docker pull " .RegistryURL "/" .Profile.Handle "/myimage") }}
  4. Browser will open for authorization - click Approve
  5. Done! Device is automatically authorized
  6. @@ -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 { diff --git a/pkg/appview/templates/partials/push-list.html b/pkg/appview/templates/partials/push-list.html index b8515e3..dcca959 100644 --- a/pkg/appview/templates/partials/push-list.html +++ b/pkg/appview/templates/partials/push-list.html @@ -17,11 +17,11 @@
- + {{ .StarCount }} - + {{ .PullCount }}
@@ -35,9 +35,8 @@
{{ .Digest }} - +
-