From bc5e2a4e20994e1f69299c9a941eb67ac9ca7880 Mon Sep 17 00:00:00 2001
From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com>
Date: Wed, 6 Aug 2025 04:18:19 +0100
Subject: [PATCH] Icon improvements
* Added support for hero icons
* Added support for specifying icon colors
* Added iconWithClass custom API func
* Refactored usage of icons across widget templates
---
internal/glance/config-fields.go | 61 +++++++++++++++++++
internal/glance/static/css/utils.css | 33 +++++++++-
.../glance/static/css/widget-bookmarks.css | 7 ---
.../static/css/widget-docker-containers.css | 26 --------
internal/glance/static/css/widget-monitor.css | 36 -----------
internal/glance/static/css/widgets.css | 2 -
internal/glance/templates/bookmarks.html | 4 +-
.../glance/templates/docker-containers.html | 12 ++--
.../glance/templates/monitor-compact.html | 4 +-
internal/glance/templates/monitor.html | 8 +--
internal/glance/widget-custom-api.go | 4 ++
11 files changed, 110 insertions(+), 87 deletions(-)
delete mode 100644 internal/glance/static/css/widget-docker-containers.css
delete mode 100644 internal/glance/static/css/widget-monitor.css
diff --git a/internal/glance/config-fields.go b/internal/glance/config-fields.go
index 22d7276..41bfa5f 100644
--- a/internal/glance/config-fields.go
+++ b/internal/glance/config-fields.go
@@ -15,6 +15,7 @@ import (
)
var hslColorFieldPattern = regexp.MustCompile(`^(?:hsla?\()?([\d\.]+)(?: |,)+([\d\.]+)%?(?: |,)+([\d\.]+)%?\)?$`)
+var inStringPropertyPattern = regexp.MustCompile(`(?m)([a-zA-Z]+)\[(.*?)\]`)
const (
hslHueMax = 360
@@ -133,9 +134,30 @@ func (d *durationField) UnmarshalYAML(node *yaml.Node) error {
type customIconField struct {
URL template.URL
+ Color string
AutoInvert bool
}
+func (h *customIconField) Elem() template.HTML {
+ return h.ElemWithClass("")
+}
+
+func (h *customIconField) ElemWithClass(class string) template.HTML {
+ if h.AutoInvert && h.Color == "" {
+ class = "flat-icon " + class
+ }
+
+ if h.Color != "" {
+ return template.HTML(
+ `
`,
+ )
+ }
+
+ return template.HTML(
+ `
`,
+ )
+}
+
func newCustomIconField(value string) customIconField {
const autoInvertPrefix = "auto-invert "
field := customIconField{}
@@ -145,6 +167,25 @@ func newCustomIconField(value string) customIconField {
value = strings.TrimPrefix(value, autoInvertPrefix)
}
+ value, properties := parseInStringProperties(value)
+
+ if color, ok := properties["color"]; ok {
+ switch color {
+ case "primary":
+ color = "var(--color-primary)"
+ case "positive":
+ color = "var(--color-positive)"
+ case "negative":
+ color = "var(--color-negative)"
+ case "base":
+ color = "var(--color-text-base)"
+ case "subdue":
+ color = "var(--color-text-subdue)"
+ }
+
+ field.Color = color
+ }
+
prefix, icon, found := strings.Cut(value, ":")
if !found {
field.URL = template.URL(value)
@@ -172,6 +213,9 @@ func newCustomIconField(value string) customIconField {
field.URL = template.URL("https://cdn.jsdelivr.net/npm/@mdi/svg@latest/svg/" + basename + ".svg")
case "sh":
field.URL = template.URL("https://cdn.jsdelivr.net/gh/selfhst/icons/" + ext + "/" + basename + "." + ext)
+ case "hi":
+ field.AutoInvert = true
+ field.URL = template.URL("https://cdn.jsdelivr.net/npm/heroicons@2.2.0/24/" + basename + ".svg")
default:
field.URL = template.URL(value)
}
@@ -189,6 +233,23 @@ func (i *customIconField) UnmarshalYAML(node *yaml.Node) error {
return nil
}
+func parseInStringProperties(value string) (string, map[string]string) {
+ properties := make(map[string]string)
+
+ value = inStringPropertyPattern.ReplaceAllStringFunc(value, func(match string) string {
+ matches := inStringPropertyPattern.FindStringSubmatch(match)
+ if len(matches) != 3 {
+ return ""
+ }
+
+ properties[matches[1]] = matches[2]
+
+ return ""
+ })
+
+ return strings.TrimSpace(value), properties
+}
+
type proxyOptionsField struct {
URL string `yaml:"url"`
AllowInsecure bool `yaml:"allow-insecure"`
diff --git a/internal/glance/static/css/utils.css b/internal/glance/static/css/utils.css
index 3bbb48c..75c75a0 100644
--- a/internal/glance/static/css/utils.css
+++ b/internal/glance/static/css/utils.css
@@ -376,7 +376,27 @@ details[open] .summary::after {
gap: 0.5rem;
}
-:root:not([data-scheme=light]) .flat-icon {
+.icon {
+ object-fit: contain;
+}
+
+.icon:not(.colored-icon) {
+ opacity: 0.9;
+ filter: grayscale(0.2);
+ transition: opacity 0.2s, filter 0.2s;
+}
+
+.icon-parent:hover .icon {
+ opacity: 1;
+ filter: none;
+}
+
+.colored-icon {
+ mask: var(--icon-url) no-repeat center / contain;
+ background-color: var(--icon-color);
+}
+
+:root:not([data-scheme=light]) .flat-icon:not(.colored-icon) {
filter: invert(1);
}
@@ -459,7 +479,6 @@ details[open] .summary::after {
filter: none;
}
-
.hide-scrollbars {
scrollbar-width: none;
}
@@ -547,6 +566,13 @@ details[open] .summary::after {
visibility: hidden;
}
+.square-18 { width: 1.8rem; height: 1.8rem; }
+.square-20 { width: 2rem; height: 2rem; }
+.square-27 { width: 2.7rem; height: 2.7rem; }
+.square-30 { width: 3rem; height: 3rem; }
+.square-32 { width: 3.2rem; height: 3.2rem; }
+.square-40 { width: 4rem; height: 4rem; }
+
.cursor-help { cursor: help; }
.rounded { border-radius: var(--border-radius); }
.break-all { word-break: break-all; }
@@ -588,8 +614,11 @@ details[open] .summary::after {
.gap-15 { gap: 1.5rem; }
.gap-20 { gap: 2rem; }
.gap-25 { gap: 2.5rem; }
+.gap-30 { gap: 3rem; }
.gap-35 { gap: 3.5rem; }
+.gap-40 { gap: 4rem; }
.gap-45 { gap: 4.5rem; }
+.gap-50 { gap: 5rem; }
.gap-55 { gap: 5.5rem; }
.margin-left-auto { margin-left: auto; }
.margin-top-3 { margin-top: 0.3rem; }
diff --git a/internal/glance/static/css/widget-bookmarks.css b/internal/glance/static/css/widget-bookmarks.css
index 7f2dabd..0e205fe 100644
--- a/internal/glance/static/css/widget-bookmarks.css
+++ b/internal/glance/static/css/widget-bookmarks.css
@@ -20,12 +20,5 @@
background-color: var(--color-widget-background-highlight);
border-radius: var(--border-radius);
padding: 0.5rem;
- opacity: 0.7;
flex-shrink: 0;
}
-
-.bookmarks-icon {
- width: 20px;
- height: 20px;
- opacity: 0.8;
-}
diff --git a/internal/glance/static/css/widget-docker-containers.css b/internal/glance/static/css/widget-docker-containers.css
deleted file mode 100644
index ae08788..0000000
--- a/internal/glance/static/css/widget-docker-containers.css
+++ /dev/null
@@ -1,26 +0,0 @@
-.docker-container-icon {
- display: block;
- filter: grayscale(0.4);
- object-fit: contain;
- aspect-ratio: 1 / 1;
- width: 2.7rem;
- opacity: 0.8;
- transition: filter 0.3s, opacity 0.3s;
-}
-
-.docker-container-icon.flat-icon {
- opacity: 0.7;
-}
-
-.docker-container:hover .docker-container-icon {
- opacity: 1;
-}
-
-.docker-container:hover .docker-container-icon:not(.flat-icon) {
- filter: grayscale(0);
-}
-
-.docker-container-status-icon {
- width: 2rem;
- height: 2rem;
-}
diff --git a/internal/glance/static/css/widget-monitor.css b/internal/glance/static/css/widget-monitor.css
deleted file mode 100644
index 8bc629b..0000000
--- a/internal/glance/static/css/widget-monitor.css
+++ /dev/null
@@ -1,36 +0,0 @@
-.monitor-site-icon {
- display: block;
- opacity: 0.8;
- filter: grayscale(0.4);
- object-fit: contain;
- aspect-ratio: 1 / 1;
- width: 3.2rem;
- position: relative;
- top: -0.1rem;
- transition: filter 0.3s, opacity 0.3s;
-}
-
-.monitor-site-icon.flat-icon {
- opacity: 0.7;
-}
-
-.monitor-site:hover .monitor-site-icon {
- opacity: 1;
-}
-
-.monitor-site:hover .monitor-site-icon:not(.flat-icon) {
- filter: grayscale(0);
-}
-
-.monitor-site-status-icon {
- flex-shrink: 0;
- margin-left: auto;
- width: 2rem;
- height: 2rem;
-}
-
-.monitor-site-status-icon-compact {
- width: 1.8rem;
- height: 1.8rem;
- flex-shrink: 0;
-}
diff --git a/internal/glance/static/css/widgets.css b/internal/glance/static/css/widgets.css
index 07b41c8..59af852 100644
--- a/internal/glance/static/css/widgets.css
+++ b/internal/glance/static/css/widgets.css
@@ -2,10 +2,8 @@
@import "widget-calendar.css";
@import "widget-clock.css";
@import "widget-dns-stats.css";
-@import "widget-docker-containers.css";
@import "widget-group.css";
@import "widget-markets.css";
-@import "widget-monitor.css";
@import "widget-reddit.css";
@import "widget-releases.css";
@import "widget-rss.css";
diff --git a/internal/glance/templates/bookmarks.html b/internal/glance/templates/bookmarks.html
index a1c9c51..43821ed 100644
--- a/internal/glance/templates/bookmarks.html
+++ b/internal/glance/templates/bookmarks.html
@@ -10,10 +10,10 @@
{{- range .Links }}
-
-
+
{{- if ne "" .Icon.URL }}
-

+ {{ .Icon.ElemWithClass "square-20" }}
{{- end }}
{{ .Title }}
diff --git a/internal/glance/templates/docker-containers.html b/internal/glance/templates/docker-containers.html
index 1e694ae..f7c8d7c 100644
--- a/internal/glance/templates/docker-containers.html
+++ b/internal/glance/templates/docker-containers.html
@@ -3,9 +3,9 @@
{{- define "widget-content" }}
{{- range .Containers }}
- -
+
-
-

+ {{ .Icon.ElemWithClass "square-27" }}
{{ .Image }}
{{ .StateText }}
@@ -47,19 +47,19 @@
{{- define "state-icon" }}
{{- if eq . "ok" }}
-