1
0
Fork 1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-07-06 14:23:20 -04:00

Compare commits

...

1 commit

Author SHA1 Message Date
Andrew Nesbitt
330a76a68a
Polish UI: lucide icons, sticky footer, hamburger nav, clickable logo
- Move third-party JS into static/vendor/ and add lucide for icons; refine
  .gitignore so embedded vendor dirs aren't caught by the Go vendor rule.
- Replace folder/file emojis in the source browser with lucide icons.
- Wrap the header logo and title in a single anchor so the icon is clickable.
- Drop the redundant "Powered by git-pkgs" footer block; add a GitHub repo
  link to the About column and bump the ecosystem count from 16+ to 17+.
- Sticky footer pattern: body is min-h-full flex column with main growing to
  fill, so the footer sits at the bottom of short pages.
- Hamburger menu under md: search and nav links collapse into a drawer
  toggled by a menu button; theme toggle stays visible at both sizes.
2026-06-07 16:41:45 +01:00
9 changed files with 94 additions and 41 deletions

4
.gitignore vendored
View file

@ -14,8 +14,8 @@ proxy
coverage.html coverage.html
coverage.txt coverage.txt
# Dependency directories # Go vendor directory (repo root only; embedded UI vendor dirs are tracked)
vendor/ /vendor/
# Go workspace file # Go workspace file
go.work go.work

View file

@ -450,7 +450,8 @@ func TestStaticFiles(t *testing.T) {
path string path string
contentTypes []string contentTypes []string
}{ }{
{"/ui/static/tailwind.js", []string{"text/javascript", "application/javascript"}}, {"/ui/static/vendor/tailwind.js", []string{"text/javascript", "application/javascript"}},
{"/ui/static/vendor/lucide.min.js", []string{"text/javascript", "application/javascript"}},
{"/ui/static/style.css", []string{"text/css"}}, {"/ui/static/style.css", []string{"text/css"}},
} }
@ -533,7 +534,7 @@ func TestDashboardWithEnrichmentStats(t *testing.T) {
body := w.Body.String() body := w.Body.String()
// Dashboard should link to Tailwind JS // Dashboard should link to Tailwind JS
if !strings.Contains(body, "/ui/static/tailwind.js") { if !strings.Contains(body, "/ui/static/vendor/tailwind.js") {
t.Error("dashboard should link to Tailwind JS") t.Error("dashboard should link to Tailwind JS")
} }

File diff suppressed because one or more lines are too long

View file

@ -11,19 +11,23 @@
<meta property="og:title" content="{{template "title" .}}"> <meta property="og:title" content="{{template "title" .}}">
<meta property="og:site_name" content="git-pkgs proxy"> <meta property="og:site_name" content="git-pkgs proxy">
{{end}} {{end}}
<script src="/ui/static/tailwind.js"></script> <script src="/ui/static/vendor/tailwind.js"></script>
<script src="/ui/static/vendor/lucide.min.js" defer></script>
<script> <script>
tailwind.config = { darkMode: 'class' } tailwind.config = { darkMode: 'class' }
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) { if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark') document.documentElement.classList.add('dark')
} }
document.addEventListener('DOMContentLoaded', () => {
if (window.lucide) lucide.createIcons({ attrs: { 'aria-hidden': 'true', focusable: 'false' } })
})
</script> </script>
{{block "head" .}}{{end}} {{block "head" .}}{{end}}
</head> </head>
<body class="h-full bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100"> <body class="min-h-full flex flex-col bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100">
{{template "header" .}} {{template "header" .}}
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <main class="flex-1 w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{{block "content" .}}{{end}} {{block "content" .}}{{end}}
</main> </main>

View file

@ -5,7 +5,12 @@
<div> <div>
<h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-3">About</h3> <h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-3">About</h3>
<p class="text-sm text-gray-600 dark:text-gray-400"> <p class="text-sm text-gray-600 dark:text-gray-400">
git-pkgs proxy is a caching proxy for package registries supporting 16+ ecosystems. git-pkgs proxy is a caching proxy for package registries supporting 17+ ecosystems.
</p>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-2">
<a href="https://github.com/git-pkgs/proxy" target="_blank" rel="noopener" class="inline-flex items-center gap-1 hover:text-gray-900 dark:hover:text-gray-100">
<i data-lucide="github" class="w-4 h-4"></i><span>github.com/git-pkgs/proxy</span>
</a>
</p> </p>
</div> </div>
<div> <div>
@ -26,11 +31,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="mt-8 pt-8 border-t border-gray-200 dark:border-gray-800">
<p class="text-xs text-center text-gray-500 dark:text-gray-400">
Powered by <a href="https://github.com/git-pkgs" class="hover:text-gray-900 dark:hover:text-gray-100" target="_blank">git-pkgs</a>
</p>
</div>
</div> </div>
</footer> </footer>
{{end}} {{end}}

View file

@ -1,31 +1,21 @@
{{define "header"}} {{define "header"}}
<header class="sticky top-0 z-50 border-b border-gray-200 dark:border-gray-800 bg-white/80 dark:bg-gray-900/80 backdrop-blur-sm"> <header class="sticky top-0 z-50 border-b border-gray-200 dark:border-gray-800 bg-white/80 dark:bg-gray-900/80 backdrop-blur-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16"> <div class="flex justify-between items-center h-16 gap-4">
<div class="flex items-center gap-2"> <a href="/ui/" class="flex items-center gap-2 text-xl font-semibold hover:text-gray-700 dark:hover:text-gray-300 shrink-0">
<svg class="w-8 h-8 text-primary-600 dark:text-primary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-8 h-8 text-primary-600 dark:text-primary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
</svg> </svg>
<a href="/ui/" class="text-xl font-semibold hover:text-gray-700 dark:hover:text-gray-300">git-pkgs proxy</a> <span>git-pkgs proxy</span>
</a>
<div class="hidden md:block flex-1 max-w-md mx-4">
{{template "search_form" .}}
</div> </div>
<div class="flex-1 max-w-md mx-8"> <nav class="hidden md:flex items-center gap-6">
<form action="/ui/search" method="get" class="relative"> {{template "nav_links" .}}
<input </nav>
type="text" <div class="flex items-center gap-2">
name="q" <button id="theme-toggle" class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors" aria-label="Toggle theme">
placeholder="Search packages..."
class="w-full px-4 py-2 pl-10 text-sm bg-gray-100 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400"
/>
<svg class="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</form>
</div>
<nav class="flex items-center gap-6">
<a href="/ui/install" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100">Install</a>
<a href="/health" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100">Health</a>
<a href="/stats" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100">API</a>
<button id="theme-toggle" class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
<svg class="w-5 h-5 hidden dark:block" fill="currentColor" viewBox="0 0 20 20"> <svg class="w-5 h-5 hidden dark:block" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/> <path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/>
</svg> </svg>
@ -33,8 +23,38 @@
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/> <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/>
</svg> </svg>
</button> </button>
<button id="nav-toggle" type="button" class="md:hidden p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors" aria-label="Toggle menu" aria-expanded="false" aria-controls="mobile-nav">
<span class="nav-icon-open"><i data-lucide="menu" class="w-5 h-5"></i></span>
<span class="nav-icon-close hidden"><i data-lucide="x" class="w-5 h-5"></i></span>
</button>
</div>
</div>
<div id="mobile-nav" class="hidden md:hidden pb-4 space-y-3">
{{template "search_form" .}}
<nav class="flex flex-col">
{{template "nav_links" .}}
</nav> </nav>
</div> </div>
</div> </div>
</header> </header>
{{end}} {{end}}
{{define "search_form"}}
<form action="/ui/search" method="get" class="relative">
<input
type="text"
name="q"
placeholder="Search packages..."
class="w-full px-4 py-2 pl-10 text-sm bg-gray-100 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400"
/>
<svg class="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</form>
{{end}}
{{define "nav_links"}}
<a href="/ui/install" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 py-2 md:py-0">Install</a>
<a href="/health" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 py-2 md:py-0">Health</a>
<a href="/stats" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 py-2 md:py-0">API</a>
{{end}}

View file

@ -9,5 +9,18 @@
localStorage.theme = 'dark'; localStorage.theme = 'dark';
} }
}); });
(function() {
const toggle = document.getElementById('nav-toggle');
const menu = document.getElementById('mobile-nav');
if (!toggle || !menu) return;
toggle.addEventListener('click', function() {
const open = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute('aria-expanded', String(!open));
menu.classList.toggle('hidden');
toggle.querySelector('.nav-icon-open').classList.toggle('hidden');
toggle.querySelector('.nav-icon-close').classList.toggle('hidden');
});
})();
</script> </script>
{{end}} {{end}}

View file

@ -100,35 +100,38 @@ function renderFileTree(files, basePath) {
if (basePath) { if (basePath) {
const parentPath = basePath.split('/').slice(0, -2).join('/'); const parentPath = basePath.split('/').slice(0, -2).join('/');
html += ` html += `
<div class="px-2 py-1 hover:bg-gray-100 dark:hover:bg-gray-800 rounded cursor-pointer text-sm" <div class="px-2 py-1 hover:bg-gray-100 dark:hover:bg-gray-800 rounded cursor-pointer text-sm flex items-center gap-2 text-gray-500 dark:text-gray-400"
onclick="loadFileTree('${escapeHTML(parentPath)}'); currentPath='${escapeHTML(parentPath)}';"> onclick="loadFileTree('${escapeHTML(parentPath)}'); currentPath='${escapeHTML(parentPath)}';">
<span class="text-gray-500 dark:text-gray-400">📁 ..</span> <i data-lucide="corner-left-up" class="w-4 h-4 shrink-0"></i><span>..</span>
</div> </div>
`; `;
} }
// Render files and directories // Render files and directories
for (const file of files) { for (const file of files) {
const icon = file.is_dir ? '📁' : '📄'; const iconName = file.is_dir ? 'folder' : 'file';
const classes = 'px-2 py-1 hover:bg-gray-100 dark:hover:bg-gray-800 rounded cursor-pointer text-sm truncate'; const iconClass = file.is_dir ? 'text-amber-500 dark:text-amber-400' : 'text-gray-500 dark:text-gray-400';
const classes = 'px-2 py-1 hover:bg-gray-100 dark:hover:bg-gray-800 rounded cursor-pointer text-sm flex items-center gap-2';
const iconHTML = `<i data-lucide="${iconName}" class="w-4 h-4 shrink-0 ${iconClass}"></i>`;
if (file.is_dir) { if (file.is_dir) {
html += ` html += `
<div class="${classes}" onclick="loadFileTree('${escapeHTML(file.path)}'); currentPath='${escapeHTML(file.path)}';"> <div class="${classes}" onclick="loadFileTree('${escapeHTML(file.path)}'); currentPath='${escapeHTML(file.path)}';">
<span>${icon} ${escapeHTML(file.name)}</span> ${iconHTML}<span class="truncate">${escapeHTML(file.name)}</span>
</div> </div>
`; `;
} else { } else {
html += ` html += `
<div class="${classes}" onclick="loadFile('${escapeHTML(file.path)}')"> <div class="${classes}" onclick="loadFile('${escapeHTML(file.path)}')">
<span>${icon} ${escapeHTML(file.name)}</span> ${iconHTML}<span class="truncate">${escapeHTML(file.name)}</span>
<span class="text-xs text-gray-500 dark:text-gray-400 ml-2">${formatSize(file.size)}</span> <span class="text-xs text-gray-500 dark:text-gray-400 ml-auto shrink-0">${formatSize(file.size)}</span>
</div> </div>
`; `;
} }
} }
container.innerHTML = html; container.innerHTML = html;
if (window.lucide) lucide.createIcons({ attrs: { 'aria-hidden': 'true', focusable: 'false' } });
} }
// Load and display file content // Load and display file content