Make the theme switcher a discreet icon, move the full choice to Settings
Nav: one small icon (sun/moon/auto) cycling system -> light -> dark -> system, replacing the 3-option pill — per feedback it was too prominent. New sprite symbols (ic-sun/ic-moon/ic-auto), themeToggleNext() picks the icon and next state from the current cookie value. Settings gets a new "Preferences" tab with the full spelled-out choice for anyone who wants to pick explicitly instead of cycling.
8 files changed
+97 −51
M
CHANGELOG.md
+4 −0
M
cmd/gitfed-web/handlers_settings.go
+20 −5
M
cmd/gitfed-web/render.go
+13 −10
M
cmd/gitfed-web/security_headers.go
+3 −3
M
cmd/gitfed-web/security_headers_test.go
+3 −3
M
cmd/gitfed-web/theme.go
+16 −0
M
internal/i18n/strings_en.go
+19 −15
M
internal/i18n/strings_fr.go
+19 −15
CHANGELOG.md
@@ -2,6 +2,10 @@
A bullet starting with `**BREAKING:**` flags a change gitfed-ctl's update wizard makes you acknowledge individually before it will let you upgrade past that version.
+## 1.2.19
+
+- The nav's theme switcher is now one small icon (sun/moon/auto) that cycles system → light → dark → system, instead of a 3-option pill — more discreet, per feedback that the earlier version was too prominent. The full spelled-out choice moved to Settings → Preferences (new tab) for anyone who wants to pick explicitly rather than cycle.
+
## 1.2.18
- Branch dropdown on the repo page: the branch name button now opens a list of every branch (a native `<details>` disclosure, no client JS needed), and picking one browses that branch's file tree instead of the default. New `ListRepoTreeAtRef`/`GetRepoFileAtRef` on `admin.Ops`, threaded through `adminrpc`. `?branch=` is validated against the repo's real branch list server-side — an unknown value 404s instead of silently falling back, same treatment as an unknown path. The README/license preview stays tied to the default branch on purpose (no ref-aware variant of those two) rather than risk showing content from the wrong branch.
cmd/gitfed-web/handlers_settings.go
@@ -17,6 +17,7 @@ var settingsTpl = newTpl("settings", `
<button type="button" data-tab="tab-profile" class="{{if eq .ActiveTab "profile"}}active{{end}}">{{icon "user"}} {{t .Lang "settings.tab_profile"}}</button>
<button type="button" data-tab="tab-keys" class="{{if eq .ActiveTab "keys"}}active{{end}}">{{icon "key"}} {{t .Lang "settings.tab_keys"}}</button>
<button type="button" data-tab="tab-password" class="{{if eq .ActiveTab "password"}}active{{end}}">{{icon "lock"}} {{t .Lang "settings.tab_password"}}</button>
+ <button type="button" data-tab="tab-preferences" class="{{if eq .ActiveTab "preferences"}}active{{end}}">{{icon "sliders"}} {{t .Lang "settings.tab_preferences"}}</button>
</div>
<div id="tab-profile" class="gf-tabpane {{if eq .ActiveTab "profile"}}active{{end}}">
@@ -75,6 +76,18 @@ var settingsTpl = newTpl("settings", `
<button type="submit">{{t .Lang "settings.change_password"}}</button>
</form>
</div>
+
+<div id="tab-preferences" class="gf-tabpane {{if eq .ActiveTab "preferences"}}active{{end}}">
+ <div class="gf-card" style="padding: 1.3rem;">
+ <strong>{{t .Lang "settings.theme_title"}}</strong>
+ <p class="muted" style="margin: 0.3rem 0 0.9rem;">{{t .Lang "settings.theme_hint"}}</p>
+ <div class="gf-lang">
+ <a href="/theme/system?next=%2Fsettings%3Ftab%3Dpreferences"{{if not .Theme}} class="active"{{end}}>{{t .Lang "theme.system"}}</a>
+ <a href="/theme/light?next=%2Fsettings%3Ftab%3Dpreferences"{{if eq .Theme "light"}} class="active"{{end}}>{{t .Lang "theme.light"}}</a>
+ <a href="/theme/dark?next=%2Fsettings%3Ftab%3Dpreferences"{{if eq .Theme "dark"}} class="active"{{end}}>{{t .Lang "theme.dark"}}</a>
+ </div>
+ </div>
+</div>
`)
type keyView struct{ Short, Full, Algo string }
@@ -85,6 +98,8 @@ func settingsTab(r *http.Request) string {
return "keys"
case "password":
return "password"
+ case "preferences":
+ return "preferences"
default:
return "profile"
}
@@ -114,11 +129,11 @@ func (s *server) handleSettings(w http.ResponseWriter, r *http.Request) {
var buf bytes.Buffer
_ = settingsTpl.Execute(&buf, struct {
- Username, Principal, ActiveTab, Lang, Initials, Bio string
- IsAdmin bool
- Keys []keyView
- Flash template.HTML
- }{sess.Username, sess.Principal, settingsTab(r), string(lang), initials(sess.Username), user.Bio, sess.IsAdmin, keys, flash(r)})
+ Username, Principal, ActiveTab, Lang, Theme, Initials, Bio string
+ IsAdmin bool
+ Keys []keyView
+ Flash template.HTML
+ }{sess.Username, sess.Principal, settingsTab(r), string(lang), s.theme(r), initials(sess.Username), user.Bio, sess.IsAdmin, keys, flash(r)})
s.render(w, r, i18n.T(lang, "settings.title"), "settings", template.HTML(buf.String()))
}
cmd/gitfed-web/render.go
@@ -161,6 +161,9 @@ const iconSprite = `<svg width="0" height="0" style="position:absolute" aria-hid
<symbol id="ic-history" viewBox="0 0 24 24"><circle cx="12" cy="12.5" r="8" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M12 8v4.7l3.3 2" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M8 3.3 5 6M16 3.3 19 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></symbol>
<symbol id="ic-user" viewBox="0 0 24 24"><circle cx="12" cy="8.3" r="3.3" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M4.8 19c1-3.2 3.9-5 7.2-5s6.2 1.8 7.2 5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></symbol>
<symbol id="ic-plus" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></symbol>
+<symbol id="ic-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4.2" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3v2.3M12 18.7V21M21 12h-2.3M5.3 12H3M18.4 5.6l-1.6 1.6M7.2 16.8l-1.6 1.6M18.4 18.4l-1.6-1.6M7.2 7.2 5.6 5.6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></symbol>
+<symbol id="ic-moon" viewBox="0 0 24 24"><path d="M20 14.5A8.5 8.5 0 1 1 9.5 4a7 7 0 0 0 10.5 10.5Z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></symbol>
+<symbol id="ic-auto" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 4a8 8 0 0 1 0 16Z" fill="currentColor"/></symbol>
</defs>
</svg>`
@@ -760,11 +763,9 @@ const shellHeadSrc = `<!doctype html>
</div>
</form>
<div class="gf-nav-right">
- <div class="gf-lang">
- <a href="/theme/system?next={{.NextPath}}"{{if not .Theme}} class="active"{{end}}>{{t .Lang "theme.system"}}</a>
- <a href="/theme/light?next={{.NextPath}}"{{if eq .Theme "light"}} class="active"{{end}}>{{t .Lang "theme.light"}}</a>
- <a href="/theme/dark?next={{.NextPath}}"{{if eq .Theme "dark"}} class="active"{{end}}>{{t .Lang "theme.dark"}}</a>
- </div>
+ <a href="/theme/{{.ThemeNext}}?next={{.NextPath}}" class="gf-bell" title="{{t .Lang "theme.toggle"}}" aria-label="{{t .Lang "theme.toggle"}}">
+ {{icon .ThemeIcon}}
+ </a>
<div class="gf-lang">
<a href="/lang/en?next={{.NextPath}}"{{if eq .Lang "en"}} class="active"{{end}}>EN</a>
<a href="/lang/fr?next={{.NextPath}}"{{if eq .Lang "fr"}} class="active"{{end}}>FR</a>
@@ -910,15 +911,17 @@ func (s *server) render(w http.ResponseWriter, r *http.Request, title, active st
pendingTrust, _ = s.ops.CountPendingTrust()
}
}
+ theme := s.theme(r)
+ themeIcon, themeNext := themeToggleNext(theme)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_ = shellTpl.Execute(w, struct {
- Title, Domain, Active, Username, Version, Initials, SearchQuery, Lang, Theme, NextPath string
- LoggedIn, IsAdmin bool
- PendingNotifs, PendingTrust int
- Body, BrandMark, IconSprite template.HTML
+ Title, Domain, Active, Username, Version, Initials, SearchQuery, Lang, Theme, ThemeIcon, ThemeNext, NextPath string
+ LoggedIn, IsAdmin bool
+ PendingNotifs, PendingTrust int
+ Body, BrandMark, IconSprite template.HTML
}{
title, s.domain, active, sess.Username, version.Version, initials(sess.Username), r.URL.Query().Get("q"),
- string(s.lang(r)), s.theme(r), r.URL.RequestURI(),
+ string(s.lang(r)), theme, themeIcon, themeNext, r.URL.RequestURI(),
loggedIn, sess.IsAdmin, pendingNotifs, pendingTrust, body, template.HTML(brandMark), template.HTML(iconSprite),
})
}
cmd/gitfed-web/security_headers.go
@@ -26,9 +26,9 @@ var scriptHash = "sha256-" + func() string {
func renderedShellScript() string {
var buf bytes.Buffer
_ = shellTpl.Execute(&buf, struct {
- Title, Domain, Active, Username, Version, Initials, SearchQuery, Lang, Theme, NextPath string
- LoggedIn, IsAdmin bool
- Body, BrandMark, IconSprite template.HTML
+ Title, Domain, Active, Username, Version, Initials, SearchQuery, Lang, Theme, ThemeIcon, ThemeNext, NextPath string
+ LoggedIn, IsAdmin bool
+ Body, BrandMark, IconSprite template.HTML
}{Lang: "en"})
html := buf.String()
open := strings.Index(html, "<script>")
cmd/gitfed-web/security_headers_test.go
@@ -17,9 +17,9 @@ import (
func TestCSPScriptHashMatchesRenderedScript(t *testing.T) {
var buf bytes.Buffer
err := shellTpl.Execute(&buf, struct {
- Title, Domain, Active, Username, Version, Initials, SearchQuery, Lang, Theme, NextPath string
- LoggedIn, IsAdmin bool
- Body, BrandMark, IconSprite template.HTML
+ Title, Domain, Active, Username, Version, Initials, SearchQuery, Lang, Theme, ThemeIcon, ThemeNext, NextPath string
+ LoggedIn, IsAdmin bool
+ Body, BrandMark, IconSprite template.HTML
}{Title: "t", Lang: "en"})
if err != nil {
t.Fatalf("execute shell: %v", err)
cmd/gitfed-web/theme.go
@@ -22,6 +22,22 @@ func (s *server) theme(r *http.Request) string {
return ""
}
+// themeToggleNext returns the icon to show for the nav's compact theme
+// button given the current theme value ("" meaning system), and the theme
+// clicking it switches to — a three-state cycle (system -> light -> dark
+// -> system) exposed as one small icon rather than the full spelled-out
+// choice, which lives in Settings instead for anyone who wants it there.
+func themeToggleNext(current string) (icon, next string) {
+ switch current {
+ case "light":
+ return "sun", "dark"
+ case "dark":
+ return "moon", "system"
+ default:
+ return "auto", "light"
+ }
+}
+
// handleSetTheme stores the chosen theme in a cookie (or clears it for
// "system") and bounces back to wherever the switcher was clicked from —
// same shape as handleSetLang.
internal/i18n/strings_en.go
@@ -2,21 +2,25 @@ package i18n
var en = map[string]string{
// ---------- nav / shell ----------
- "nav.home": "Home",
- "nav.explore": "Explore",
- "nav.security": "Security",
- "nav.dashboard": "My repos",
- "nav.search_placeholder": "Search repos…",
- "nav.login": "Log in",
- "nav.signed_in_as": "Signed in as",
- "nav.settings": "Settings",
- "nav.admin": "Admin",
- "nav.logout": "Log out",
- "nav.notifications": "Notifications",
- "theme.system": "Auto",
- "theme.light": "Light",
- "theme.dark": "Dark",
- "footer.changelog": "Gitfed %s",
+ "nav.home": "Home",
+ "nav.explore": "Explore",
+ "nav.security": "Security",
+ "nav.dashboard": "My repos",
+ "nav.search_placeholder": "Search repos…",
+ "nav.login": "Log in",
+ "nav.signed_in_as": "Signed in as",
+ "nav.settings": "Settings",
+ "nav.admin": "Admin",
+ "nav.logout": "Log out",
+ "nav.notifications": "Notifications",
+ "theme.system": "Auto",
+ "theme.light": "Light",
+ "theme.dark": "Dark",
+ "theme.toggle": "Toggle theme",
+ "settings.tab_preferences": "Preferences",
+ "settings.theme_title": "Theme",
+ "settings.theme_hint": "Auto follows your system's setting. This also sets the small toggle in the nav.",
+ "footer.changelog": "Gitfed %s",
// ---------- explore ----------
"explore.title": "Explore public repositories",
internal/i18n/strings_fr.go
@@ -2,21 +2,25 @@ package i18n
var fr = map[string]string{
// ---------- nav / shell ----------
- "nav.home": "Accueil",
- "nav.explore": "Explorer",
- "nav.security": "Sécurité",
- "nav.dashboard": "Mes repos",
- "nav.search_placeholder": "Rechercher un dépôt…",
- "nav.login": "Se connecter",
- "nav.signed_in_as": "Connecté en tant que",
- "nav.settings": "Paramètres",
- "nav.admin": "Administration",
- "nav.logout": "Se déconnecter",
- "nav.notifications": "Notifications",
- "theme.system": "Auto",
- "theme.light": "Clair",
- "theme.dark": "Sombre",
- "footer.changelog": "Gitfed %s",
+ "nav.home": "Accueil",
+ "nav.explore": "Explorer",
+ "nav.security": "Sécurité",
+ "nav.dashboard": "Mes repos",
+ "nav.search_placeholder": "Rechercher un dépôt…",
+ "nav.login": "Se connecter",
+ "nav.signed_in_as": "Connecté en tant que",
+ "nav.settings": "Paramètres",
+ "nav.admin": "Administration",
+ "nav.logout": "Se déconnecter",
+ "nav.notifications": "Notifications",
+ "theme.system": "Auto",
+ "theme.light": "Clair",
+ "theme.dark": "Sombre",
+ "theme.toggle": "Changer de thème",
+ "settings.tab_preferences": "Préférences",
+ "settings.theme_title": "Thème",
+ "settings.theme_hint": "Auto suit le réglage de ton système. Ça change aussi le petit bouton dans la barre de navigation.",
+ "footer.changelog": "Gitfed %s",
// ---------- explore ----------
"explore.title": "Explorer les dépôts publics",