diff --git a/modules/svg/svg.go b/modules/svg/svg.go
index 234b1f8c13..917df9439a 100644
--- a/modules/svg/svg.go
+++ b/modules/svg/svg.go
@@ -7,6 +7,7 @@ import (
"fmt"
"html/template"
"path"
+ "sort"
"strings"
"sync"
@@ -78,6 +79,16 @@ func MockIcon(icon string) func() {
}
}
+// DiscoveredIconNames returns the sorted list of all discovered SVG icon names
+func DiscoveredIconNames() []string {
+ names := make([]string, 0, len(svgIcons))
+ for name := range svgIcons {
+ names = append(names, name)
+ }
+ sort.Strings(names)
+ return names
+}
+
// RenderHTML renders icons - arguments icon name (string), size (int), class (string)
func RenderHTML(icon string, others ...any) template.HTML {
result, _ := renderHTML(icon, others...)
diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go
index 8283d3ad9d..7b1d2d1271 100644
--- a/routers/web/devtest/devtest.go
+++ b/routers/web/devtest/devtest.go
@@ -18,6 +18,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/badge"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/svg"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
@@ -180,6 +181,34 @@ func prepareMockDataRelativeTime(ctx *context.Context) {
ctx.Data["TimeFuture1y"] = now.Add(366 * 24 * time.Hour)
}
+func prepareMockDataAvatar(ctx *context.Context) {
+ mockUsers, _ := db.Find[user_model.User](ctx, user_model.SearchUserOptions{ListOptions: db.ListOptions{PageSize: 1}})
+ ctx.Data["MockUser"] = mockUsers[0]
+ ctx.Data["AvatarSizes"] = []int{16, 20, 24, 28, 32, 40, 48, 64, 100, 128}
+ ctx.Data["SampleEmails"] = []string{
+ "alice@example.com",
+ "bob@example.org",
+ "charlie@test.io",
+ "devtest@gitea.io",
+ "noreply@example.com",
+ }
+}
+
+func prepareMockDataIconGallery(ctx *context.Context) {
+ allNames := svg.DiscoveredIconNames()
+ grouped := map[string][]string{}
+ for _, name := range allNames {
+ prefix := "other"
+ if before, _, ok := strings.Cut(name, "-"); ok {
+ prefix = before
+ }
+ grouped[prefix] = append(grouped[prefix], name)
+ }
+ ctx.Data["IconGroups"] = grouped
+ ctx.Data["IconGroupOrder"] = []string{"octicon", "gitea", "fontawesome", "material", "other"}
+ ctx.Data["IconCount"] = len(allNames)
+}
+
func prepareMockData(ctx *context.Context) {
switch ctx.Req.URL.Path {
case "/devtest/gitea-ui":
@@ -190,6 +219,10 @@ func prepareMockData(ctx *context.Context) {
prepareMockDataBadgeActionsSvg(ctx)
case "/devtest/relative-time":
prepareMockDataRelativeTime(ctx)
+ case "/devtest/avatar":
+ prepareMockDataAvatar(ctx)
+ case "/devtest/icon-gallery":
+ prepareMockDataIconGallery(ctx)
}
}
diff --git a/templates/devtest/avatar.tmpl b/templates/devtest/avatar.tmpl
new file mode 100644
index 0000000000..3c48675f63
--- /dev/null
+++ b/templates/devtest/avatar.tmpl
@@ -0,0 +1,83 @@
+{{template "devtest/devtest-header"}}
+
+
Avatar
+
+
Sizes (via AvatarByEmail)
+
Using ctx.AvatarUtils.AvatarByEmail at various pixel sizes:
+
+ {{range $size := .AvatarSizes}}
+
+ {{ctx.AvatarUtils.AvatarByEmail "devtest@example.com" "Dev Test" $size}}
+ {{$size}}px
+
+ {{end}}
+
+
+
User Avatars
+
Using ctx.AvatarUtils.Avatar with a real user and with nil (fallback):
+
+
+ {{ctx.AvatarUtils.Avatar .MockUser 28}}
+ User (28px)
+
+
+ {{ctx.AvatarUtils.Avatar .MockUser 40}}
+ User (40px)
+
+
+ {{ctx.AvatarUtils.Avatar .MockUser 100}}
+ User (100px)
+
+
+ {{ctx.AvatarUtils.Avatar nil 28}}
+ nil fallback (28px)
+
+
+ {{ctx.AvatarUtils.Avatar nil 40}}
+ nil fallback (40px)
+
+
+
+
Custom CSS Classes
+
+
+ {{ctx.AvatarUtils.Avatar .MockUser 28 "ui avatar tw-align-middle"}}
+ default class
+
+
+ {{ctx.AvatarUtils.Avatar .MockUser 28 "ui avatar tw-align-middle tw-rounded-full"}}
+ tw-rounded-full
+
+
+
+
Inline with Text
+
+
{{ctx.AvatarUtils.Avatar .MockUser 20 "ui avatar tw-align-middle"}} {{.MockUser.Name}} opened this issue
+
+
+ {{ctx.AvatarUtils.AvatarByEmail "user1@example.com" "User One" 16}} User One and {{ctx.AvatarUtils.AvatarByEmail "user2@example.com" "User Two" 16}} User Two
+
+
+
Avatar with Link (typical pattern)
+
+
+
Avatar Upload Cropper
+
The cropper requires index.js (global init). The HTML structure is shown below for reference:
+
+ {{template "shared/avatar_upload_crop" dict "LabelText" "Choose an avatar"}}
+
+
+
Multiple Email Avatars (gravatar hashing)
+
Different emails produce different fallback avatars:
+
+ {{range $email := .SampleEmails}}
+
+ {{ctx.AvatarUtils.AvatarByEmail $email $email 28}}
+ {{$email}}
+
+ {{end}}
+
+
+{{template "devtest/devtest-footer"}}
diff --git a/templates/devtest/icon-gallery.tmpl b/templates/devtest/icon-gallery.tmpl
new file mode 100644
index 0000000000..fd24bf7b34
--- /dev/null
+++ b/templates/devtest/icon-gallery.tmpl
@@ -0,0 +1,45 @@
+{{template "devtest/devtest-header"}}
+
+
Icon Gallery
+
All {{.IconCount}} SVG icons available in templates.
+
+
+ Large (24px)
+
+
+
+
+ {{range $prefix := .IconGroupOrder}}
+ {{$icons := index $.IconGroups $prefix}}
+ {{if $icons}}
+
{{$prefix}} {{len $icons}}
+
+ {{range $name := $icons}}
+
+
{{svg $name 16}}
+
{{$name}}
+
+ {{end}}
+
+ {{end}}
+ {{end}}
+
+{{template "devtest/devtest-footer"}}