Fix unreadable event text when a calendar's color is white/very light
The month view rendered each event's text (and its little leading dot) in the calendar's own color, on a very lightly tinted (~13% opacity) background of that same color. For a light color like white or pale yellow, the text ended up effectively invisible against its own near-white background. internal/web/templates/calendar.templ: add eventTextColor(hex), which darkens colors above a perceived-luminance threshold (keeping the hue, e.g. white -> a mid gray, pale yellow -> olive) while leaving already- legible colors untouched; used for both the event text and its leading dot. Also add a subtle ring to the event dot and to the calendar legend's color swatch so a white/near-white swatch stays visible against the page's white background, independent of the text-color fix. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package templates
|
||||
|
||||
import "fmt"
|
||||
import "strconv"
|
||||
import "strings"
|
||||
|
||||
// CalendarSummary is one calendar (own or shared) shown in the combined
|
||||
// month view's legend.
|
||||
@@ -119,7 +121,7 @@ templ MonthView(username string, data MonthViewData) {
|
||||
|
||||
templ calendarLegendRow(c CalendarSummary) {
|
||||
<div class="px-4 py-3 flex items-center gap-3 text-sm flex-wrap">
|
||||
<span class="w-3 h-3 rounded-full shrink-0" style={ "background-color: " + colorOrDefault(c.Color) }></span>
|
||||
<span class="w-3 h-3 rounded-full shrink-0 ring-1 ring-inset ring-black/10" style={ "background-color: " + colorOrDefault(c.Color) }></span>
|
||||
<span class="font-medium">{ c.Name }</span>
|
||||
if c.Shared {
|
||||
<span class="text-xs text-gray-500">shared by { c.Owner }</span>
|
||||
@@ -155,10 +157,10 @@ templ monthDayCell(day MonthDay) {
|
||||
<a
|
||||
href={ templ.URL(eventLinkURL(ev)) }
|
||||
class="block truncate rounded px-1.5 py-0.5 text-xs hover:opacity-80"
|
||||
style={ "background-color: " + colorOrDefault(ev.Color) + "22; color: " + colorOrDefault(ev.Color) }
|
||||
style={ "background-color: " + colorOrDefault(ev.Color) + "22; color: " + eventTextColor(ev.Color) }
|
||||
title={ ev.Summary }
|
||||
>
|
||||
<span class="inline-block w-1.5 h-1.5 rounded-full mr-1" style={ "background-color: " + colorOrDefault(ev.Color) }></span>
|
||||
<span class="inline-block w-1.5 h-1.5 rounded-full mr-1 ring-1 ring-inset ring-black/10" style={ "background-color: " + eventTextColor(ev.Color) }></span>
|
||||
if !ev.AllDay && ev.TimeText != "" {
|
||||
<span class="font-medium">{ ev.TimeText }</span>
|
||||
}
|
||||
@@ -175,6 +177,55 @@ func eventLinkURL(ev EventSummary) string {
|
||||
return "/web/calendar/" + ev.CalRef + "/" + ev.ID + "/edit"
|
||||
}
|
||||
|
||||
// eventTextColor returns a color derived from hex, darkened if needed so
|
||||
// it stays legible as text on monthDayCell's very light (~13% opacity)
|
||||
// tinted event background. Without this, a light calendar color like
|
||||
// white or pale yellow renders its own event text as invisible or
|
||||
// near-invisible, since the same color is used for both the tint and the
|
||||
// text. Colors that are already dark enough are returned unchanged.
|
||||
func eventTextColor(hex string) string {
|
||||
c := colorOrDefault(hex)
|
||||
r, g, b, ok := parseHexColor(c)
|
||||
if !ok {
|
||||
return c
|
||||
}
|
||||
// Perceived luminance (ITU-R BT.601).
|
||||
luminance := 0.299*float64(r) + 0.587*float64(g) + 0.114*float64(b)
|
||||
const maxReadableLuminance = 170
|
||||
if luminance <= maxReadableLuminance {
|
||||
return c
|
||||
}
|
||||
// Scale each channel down so the color darkens towards black while
|
||||
// keeping its hue, ending up comfortably below the threshold.
|
||||
scale := maxReadableLuminance / luminance * 0.75
|
||||
return fmt.Sprintf("#%02x%02x%02x", clampByte(float64(r)*scale), clampByte(float64(g)*scale), clampByte(float64(b)*scale))
|
||||
}
|
||||
|
||||
// parseHexColor parses a "#rrggbb" string into its red/green/blue
|
||||
// components. ok is false for anything else (including 3-digit shorthand
|
||||
// hex, which colorOrDefault/hexColorRe never produce).
|
||||
func parseHexColor(s string) (r, g, b int, ok bool) {
|
||||
s = strings.TrimPrefix(s, "#")
|
||||
if len(s) != 6 {
|
||||
return 0, 0, 0, false
|
||||
}
|
||||
v, err := strconv.ParseInt(s, 16, 32)
|
||||
if err != nil {
|
||||
return 0, 0, 0, false
|
||||
}
|
||||
return int(v >> 16 & 0xff), int(v >> 8 & 0xff), int(v & 0xff), true
|
||||
}
|
||||
|
||||
func clampByte(v float64) int {
|
||||
if v < 0 {
|
||||
return 0
|
||||
}
|
||||
if v > 255 {
|
||||
return 255
|
||||
}
|
||||
return int(v)
|
||||
}
|
||||
|
||||
|
||||
|
||||
templ EventForm(username string, data EventFormData, errMsg string) {
|
||||
|
||||
Reference in New Issue
Block a user