Full-width add-repo panel; centered branded login page
Add-repo panel: split the trigger button out of the <details> element so it no longer has to size itself as a flex sibling of the button — it's now a separate full-width card below the header row, matching the width of the repo list and stat row (was capped at 480px, floating awkwardly next to empty space). Needed a small new data-toggle click handler in the shared script since native <details>/<summary> can't express "button in one place, panel spanning something else" — same pattern already used for the nav burger/profile menu toggles. Login page: centered branded card (logo, tagline, form, note) instead of a bare card dropped at the top of the page — option D from the mockup.
3 files changed
+66 −46
M
cmd/gitfed-web/handlers_auth.go
+19 −14
M
cmd/gitfed-web/handlers_dashboard.go
+28 −29
M
cmd/gitfed-web/render.go
+19 −3
cmd/gitfed-web/handlers_auth.go
@@ -11,17 +11,22 @@ import (
)
var loginTpl = newTpl("login", `
-{{.Flash}}
-<form class="card narrow" method="post" action="/login">
- <strong>{{t .Lang "auth.login"}}</strong>
- <input type="hidden" name="next" value="{{.Next}}">
- <label>{{t .Lang "auth.username"}}</label>
- <input name="username" required autofocus>
- <label>{{t .Lang "auth.password"}}</label>
- <input name="password" type="password" required>
- <button type="submit">{{t .Lang "auth.login"}}</button>
-</form>
-<p class="muted">{{t .Lang "auth.note"}}</p>
+<div class="gf-auth-center">
+ <div class="gf-auth-box">
+ <div class="gf-auth-brand">{{.BrandMark}} Gitfed</div>
+ <p class="gf-auth-tagline">{{t .Lang "landing.kicker"}}</p>
+ {{.Flash}}
+ <form class="card" method="post" action="/login">
+ <input type="hidden" name="next" value="{{.Next}}">
+ <label>{{t .Lang "auth.username"}}</label>
+ <input name="username" required autofocus>
+ <label>{{t .Lang "auth.password"}}</label>
+ <input name="password" type="password" required>
+ <button type="submit">{{t .Lang "auth.login"}}</button>
+ </form>
+ <p class="muted gf-auth-note">{{t .Lang "auth.note"}}</p>
+ </div>
+</div>
`)
func (s *server) handleLoginForm(w http.ResponseWriter, r *http.Request) {
@@ -33,9 +38,9 @@ func (s *server) handleLoginForm(w http.ResponseWriter, r *http.Request) {
next := sanitizeNext(r.URL.Query().Get("next"))
var buf bytes.Buffer
_ = loginTpl.Execute(&buf, struct {
- Next, Lang string
- Flash template.HTML
- }{next, string(lang), flash(r)})
+ Next, Lang string
+ Flash, BrandMark template.HTML
+ }{next, string(lang), flash(r), template.HTML(brandMark)})
s.render(w, r, i18n.T(lang, "auth.login"), "login", template.HTML(buf.String()))
}
cmd/gitfed-web/handlers_dashboard.go
@@ -21,37 +21,36 @@ var dashboardTpl = newTpl("dashboard", `
<div class="gf-page-head">
<h1>{{t .Lang "dashboard.title"}}</h1>
- <details class="gf-new-repo">
- <summary class="gf-btn primary" style="margin:0;">+ {{t .Lang "dashboard.add_repo"}}</summary>
- <div class="gf-card gf-add-repo-panel" style="margin-top:0.75rem;">
- <div class="gf-tabs">
- <button type="button" data-tab="tab-new-repo" class="active">{{t .Lang "dashboard.new_repo"}}</button>
- <button type="button" data-tab="tab-import-repo">{{t .Lang "dashboard.import_repo"}}</button>
- </div>
- <div id="tab-new-repo" class="gf-tabpane active">
- <form method="post" action="/repos">
- <label>{{t .Lang "dashboard.repo_name"}}</label>
- <div class="gf-input-group">
- <span class="prefix">{{.Username}}/</span>
- <input name="name" required placeholder="my-project" autocomplete="off">
- </div>
- <button type="submit">{{t .Lang "dashboard.create"}}</button>
- </form>
+ <button type="button" class="gf-btn primary" data-toggle="addRepoPanel" aria-expanded="false" aria-controls="addRepoPanel">+ {{t .Lang "dashboard.add_repo"}}</button>
+</div>
+
+<div class="gf-card gf-add-repo-panel" id="addRepoPanel" hidden>
+ <div class="gf-tabs">
+ <button type="button" data-tab="tab-new-repo" class="active">{{t .Lang "dashboard.new_repo"}}</button>
+ <button type="button" data-tab="tab-import-repo">{{t .Lang "dashboard.import_repo"}}</button>
+ </div>
+ <div id="tab-new-repo" class="gf-tabpane active">
+ <form method="post" action="/repos">
+ <label>{{t .Lang "dashboard.repo_name"}}</label>
+ <div class="gf-input-group">
+ <span class="prefix">{{.Username}}/</span>
+ <input name="name" required placeholder="my-project" autocomplete="off">
</div>
- <div id="tab-import-repo" class="gf-tabpane gf-import-form">
- <form method="post" action="/repos/import">
- <label>{{t .Lang "dashboard.import_url_label"}}</label>
- <input name="source_url" type="url" required placeholder="https://github.com/owner/repo.git" id="importUrl" autocomplete="off">
- <label>{{t .Lang "dashboard.repo_name"}}</label>
- <div class="gf-input-group">
- <span class="prefix">{{.Username}}/</span>
- <input name="name" required placeholder="repo" id="importName" autocomplete="off">
- </div>
- <button type="submit">{{t .Lang "dashboard.import"}}</button>
- </form>
+ <button type="submit">{{t .Lang "dashboard.create"}}</button>
+ </form>
+ </div>
+ <div id="tab-import-repo" class="gf-tabpane gf-import-form">
+ <form method="post" action="/repos/import">
+ <label>{{t .Lang "dashboard.import_url_label"}}</label>
+ <input name="source_url" type="url" required placeholder="https://github.com/owner/repo.git" id="importUrl" autocomplete="off">
+ <label>{{t .Lang "dashboard.repo_name"}}</label>
+ <div class="gf-input-group">
+ <span class="prefix">{{.Username}}/</span>
+ <input name="name" required placeholder="repo" id="importName" autocomplete="off">
</div>
- </div>
- </details>
+ <button type="submit">{{t .Lang "dashboard.import"}}</button>
+ </form>
+ </div>
</div>
<div class="gf-card gf-repo-list">
cmd/gitfed-web/render.go
@@ -488,9 +488,10 @@ const shellHeadSrc = `<!doctype html>
.gf-new-repo summary::-webkit-details-marker { display: none; }
.gf-new-repo[open] summary { margin-bottom: 0.5rem; }
.gf-new-repo form.card { min-width: 320px; }
- .gf-add-repo-panel { min-width: 360px; max-width: 480px; }
+ .gf-add-repo-panel { margin-bottom: 1.25rem; }
.gf-add-repo-panel .gf-tabs { margin: 0; padding: 0.3rem 1.1rem 0; }
.gf-add-repo-panel .gf-tabpane { padding: 1.1rem; }
+ .gf-add-repo-panel .gf-tabpane form { max-width: 480px; }
.gf-input-group { display: flex; align-items: stretch; margin-top: 0.25rem; }
.gf-input-group input { margin-top: 0; border-radius: 0 6px 6px 0; }
@@ -575,7 +576,6 @@ const shellHeadSrc = `<!doctype html>
/* ---------- forms & buttons ---------- */
form.inline { display: inline; }
form.card { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 1.1rem; margin: 1rem 0; }
- form.card.narrow { max-width: 480px; }
form.card label { display: block; font-size: 0.85rem; color: var(--text-dim); margin-top: 0.6rem; }
input, select, textarea { width: 100%; box-sizing: border-box; padding: 0.45rem 0.6rem; margin-top: 0.25rem; background: var(--canvas); border: 1px solid var(--border-strong); color: var(--text); border-radius: 6px; font-size: 0.9rem; font-family: inherit; }
textarea { resize: vertical; line-height: 1.5; }
@@ -599,6 +599,14 @@ const shellHeadSrc = `<!doctype html>
.badge.plain { background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); margin-right: 0.3rem; }
.badge.danger { background: var(--danger-bg); color: var(--danger-fg); }
.muted { color: var(--text-dim); font-size: 0.85rem; }
+
+ /* ---------- auth (login) ---------- */
+ .gf-auth-center { display: flex; align-items: center; justify-content: center; min-height: 60vh; padding: 2rem 0; }
+ .gf-auth-box { width: 100%; max-width: 340px; text-align: center; }
+ .gf-auth-brand { display: flex; align-items: center; justify-content: center; gap: 0.5rem; color: var(--accent); font-weight: 700; font-size: 1.15rem; margin-bottom: 0.4rem; }
+ .gf-auth-tagline { color: var(--text-faint); font-size: 0.82rem; margin: 0 0 1.6rem; }
+ .gf-auth-box form.card { text-align: left; margin: 0; }
+ .gf-auth-note { margin: 1.2rem auto 0; font-size: 0.78rem; }
code, pre { background: var(--surface-2); border-radius: 6px; font-family: var(--mono); font-size: 0.86em; }
code { padding: 0.15rem 0.4rem; }
pre { padding: 1rem; overflow-x: auto; border: 1px solid var(--border); line-height: 1.5; }
@@ -629,7 +637,6 @@ const shellHeadSrc = `<!doctype html>
@media (max-width: 480px) {
main { padding: 1rem 0.85rem; }
- form.card.narrow { max-width: none; }
}
</style>
</head>
@@ -708,6 +715,15 @@ const shellScriptJS = `
burger.setAttribute('aria-expanded', open ? 'true' : 'false');
});
}
+ document.querySelectorAll('[data-toggle]').forEach(function (btn) {
+ var panel = document.getElementById(btn.getAttribute('data-toggle'));
+ if (!panel) return;
+ btn.addEventListener('click', function () {
+ var open = panel.hidden;
+ panel.hidden = !open;
+ btn.setAttribute('aria-expanded', open ? 'true' : 'false');
+ });
+ });
var profileTrigger = document.getElementById('profileTrigger');
var profileMenu = document.getElementById('profileMenu');
if (profileTrigger && profileMenu) {