Gitfed
bastien-mrq/gitfed/ Commits/ d500b82

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.

bastien-mrq 2026-07-30 13:13 commit d500b82faa1dae5311e953a914d4b690474441c3 parent 62184be6fb081863bdf227b3a064c29a339ba728
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
diff --git a/CHANGELOG.md b/CHANGELOG.md index 72ce6a5..927d319 100644 --- a/CHANGELOG.md +++ b/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
diff --git a/cmd/gitfed-web/handlers_settings.go b/cmd/gitfed-web/handlers_settings.go index fe30c69..56d8719 100644 --- a/cmd/gitfed-web/handlers_settings.go +++ b/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
diff --git a/cmd/gitfed-web/render.go b/cmd/gitfed-web/render.go index ebd9bb2..8fafaf7 100644 --- a/cmd/gitfed-web/render.go +++ b/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
diff --git a/cmd/gitfed-web/security_headers.go b/cmd/gitfed-web/security_headers.go index c94a736..81a667f 100644 --- a/cmd/gitfed-web/security_headers.go +++ b/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
diff --git a/cmd/gitfed-web/security_headers_test.go b/cmd/gitfed-web/security_headers_test.go index 8bdf055..de7ca07 100644 --- a/cmd/gitfed-web/security_headers_test.go +++ b/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
diff --git a/cmd/gitfed-web/theme.go b/cmd/gitfed-web/theme.go index 206a6f1..20c1704 100644 --- a/cmd/gitfed-web/theme.go +++ b/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
diff --git a/internal/i18n/strings_en.go b/internal/i18n/strings_en.go index 6beecfe..aba7203 100644 --- a/internal/i18n/strings_en.go +++ b/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
diff --git a/internal/i18n/strings_fr.go b/internal/i18n/strings_fr.go index d5df62c..a1571f6 100644 --- a/internal/i18n/strings_fr.go +++ b/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",