catch mid colors outside of WCAG AA

This commit is contained in:
41666 2025-04-06 17:28:26 -07:00
parent f72c7a357b
commit df33164b08
28 changed files with 135 additions and 96 deletions

View file

@ -3,7 +3,7 @@ package authmiddleware_test
import (
"testing"
"git.sapphic.engineer/roleypoly/v4/authmiddleware"
"git.sapphic.engineer/roleypoly/v4/auth/authmiddleware"
"git.sapphic.engineer/roleypoly/v4/discord/clientmock"
"git.sapphic.engineer/roleypoly/v4/types"
"git.sapphic.engineer/roleypoly/v4/types/fixtures"

View file

@ -1,11 +1,11 @@
package authmiddleware
import (
"encoding/json"
"log"
"time"
"git.sapphic.engineer/roleypoly/v4/types"
"github.com/goccy/go-json"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/session"
)

View file

@ -2,16 +2,16 @@ package authmiddleware_test
import (
"bytes"
"encoding/json"
"errors"
"net/http"
"testing"
"git.sapphic.engineer/roleypoly/v4/authmiddleware"
"git.sapphic.engineer/roleypoly/v4/auth/authmiddleware"
"git.sapphic.engineer/roleypoly/v4/discord"
"git.sapphic.engineer/roleypoly/v4/discord/clientmock"
"git.sapphic.engineer/roleypoly/v4/roleypoly"
"git.sapphic.engineer/roleypoly/v4/types"
"github.com/goccy/go-json"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/session"
"github.com/stretchr/testify/assert"

View file

@ -3,7 +3,7 @@ package authmiddleware_test
import (
"testing"
"git.sapphic.engineer/roleypoly/v4/authmiddleware"
"git.sapphic.engineer/roleypoly/v4/auth/authmiddleware"
"git.sapphic.engineer/roleypoly/v4/discord/clientmock"
"git.sapphic.engineer/roleypoly/v4/types/fixtures"
"github.com/stretchr/testify/assert"

View file

@ -0,0 +1 @@
package auth

View file

@ -1,6 +1,6 @@
{
pkgs ? import <nixpkgs> {},
vendorHash ? "sha256-ilEuRNE61UN22Jm6Yyv80S6VdRa1mB6J/Pde1x/DgEk=",
vendorHash ? "sha256-19z+/CD45jtKSCOooCQaVX4YvMFSp+aDaUIXlLMPLkA=",
}:
rec {
default = roleypoly;

View file

@ -1,13 +1,12 @@
package discord
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"time"
"github.com/goccy/go-json"
)
const DiscordBaseUrl = "https://discord.com/api/v10"

1
go.mod
View file

@ -3,7 +3,6 @@ module git.sapphic.engineer/roleypoly/v4
go 1.24.1
require (
github.com/goccy/go-json v0.10.5
github.com/gofiber/fiber/v3 v3.0.0-beta.4
github.com/gofiber/template/html/v2 v2.1.3
github.com/stretchr/testify v1.10.0

2
go.sum
View file

@ -5,8 +5,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gofiber/fiber/v3 v3.0.0-beta.4 h1:KzDSavvhG7m81NIsmnu5l3ZDbVS4feCidl4xlIfu6V0=
github.com/gofiber/fiber/v3 v3.0.0-beta.4/go.mod h1:/WFUoHRkZEsGHyy2+fYcdqi109IVOFbVwxv1n1RU+kk=
github.com/gofiber/schema v1.2.0 h1:j+ZRrNnUa/0ZuWrn/6kAtAufEr4jCJ+JuTURAMxNSZg=

View file

@ -4,6 +4,7 @@ import (
"bytes"
"crypto/ed25519"
"encoding/hex"
"encoding/json"
"net/http"
"testing"
@ -11,7 +12,6 @@ import (
"git.sapphic.engineer/roleypoly/v4/discord"
"git.sapphic.engineer/roleypoly/v4/interactions"
"git.sapphic.engineer/roleypoly/v4/roleypoly"
"github.com/goccy/go-json"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/session"
"github.com/stretchr/testify/assert"

View file

@ -5,6 +5,7 @@ import (
"crypto"
"crypto/ed25519"
"encoding/hex"
"encoding/json"
"net/http"
"testing"
"time"
@ -13,7 +14,6 @@ import (
"git.sapphic.engineer/roleypoly/v4/discord/clientmock"
"git.sapphic.engineer/roleypoly/v4/interactions"
"git.sapphic.engineer/roleypoly/v4/types/fixtures"
"github.com/goccy/go-json"
"github.com/gofiber/fiber/v3"
"github.com/stretchr/testify/assert"
)

View file

@ -3,9 +3,9 @@ package interactions_test
import (
"crypto/ed25519"
"encoding/hex"
"encoding/json"
"testing"
"github.com/goccy/go-json"
"github.com/stretchr/testify/assert"
)

View file

@ -18,7 +18,7 @@ fmt:
go fmt ./...
prettier:
prettier -w -c **/*.{css,html,json,md}
prettier -w -c "**/*.{css,json,md}"
tidy:
go mod tidy

View file

@ -40,9 +40,8 @@ func Role(category *types.Category, role *types.Role, selected bool) Presentable
}
type PresentableRoleColors struct {
Main string
Alt string
IsDark bool
Main string
Alt string
}
func GetColors(roleColor uint32) PresentableRoleColors {
@ -52,8 +51,7 @@ func GetColors(roleColor uint32) PresentableRoleColors {
altR, altG, altB := utils.AltColor(r, g, b)
return PresentableRoleColors{
Main: utils.RgbToString(r, g, b),
Alt: utils.RgbToString(altR, altG, altB),
IsDark: utils.IsDarkColor(r, g, b),
Main: utils.RgbToString(r, g, b),
Alt: utils.RgbToString(altR, altG, altB),
}
}

View file

@ -14,7 +14,6 @@ func TestRole(t *testing.T) {
assert.Equal(t, fixtures.RoleWithDarkColor.Name, r.Name)
assert.Equal(t, presentation.InputCheckbox, r.InputType)
assert.Equal(t, "#a20000", r.Colors.Main)
assert.True(t, r.Colors.IsDark)
assert.True(t, r.Selected)
r = presentation.Role(&fixtures.CategorySingle, &fixtures.RoleWithDarkColor, false)
@ -22,12 +21,11 @@ func TestRole(t *testing.T) {
assert.False(t, r.Selected)
r = presentation.Role(&fixtures.CategorySingle, &fixtures.RoleWithLightColor, true)
assert.False(t, r.Colors.IsDark)
assert.True(t, r.Selected)
}
func TestGetColors(t *testing.T) {
c := presentation.GetColors(0xa20000)
assert.Equal(t, "#a20000", c.Main)
assert.True(t, c.IsDark)
assert.Equal(t, "#ffd8d8", c.Alt)
}

View file

@ -6,7 +6,6 @@ import (
"strings"
"time"
"github.com/goccy/go-json"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/csrf"
"github.com/gofiber/fiber/v3/middleware/session"
@ -26,8 +25,6 @@ func CreateFiberApp() *fiber.App {
viewEngine := html.NewFileSystem(http.FS(templates.FS), ".html")
app := fiber.New(fiber.Config{
JSONEncoder: json.Marshal,
JSONDecoder: json.Unmarshal,
Views: viewEngine,
ViewsLayout: "layouts/main",
})

4
static/images/x.svg Normal file
View file

@ -0,0 +1,4 @@
<svg width="16" height="16" viewbox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<rect fill="#000" width="2" height="10" x="7" y="3" style="transform: rotate(45deg); transform-origin: center;"/>
<rect fill="#000" width="2" height="10" x="7" y="3" style="transform: rotate(-45deg); transform-origin: center;"/>
</svg>

After

Width:  |  Height:  |  Size: 323 B

View file

@ -41,8 +41,8 @@ body {
border: 2px solid var(--role-color);
border-radius: 3px;
user-select: none;
padding: 0.217rem; /* this is silly number pls ignore :3 (^noe) */
gap: 0.215rem;
padding: 0.369rem; /* this is silly number pls ignore :3 (^noe) */
gap: 0.269rem;
cursor: pointer;
transition: all 0.35s ease-in-out;
@ -84,6 +84,7 @@ body {
label {
cursor: pointer;
font-weight: 600;
}
&:has(input:checked) {

View file

@ -2,7 +2,7 @@
<div
class="role"
style="--role-color: {{.Colors.Main}}; --contrast-color: {{.Colors.Alt}};"
data-testid="role-{{.ID}}"
data-testid="{{$for}}"
>
<input type="{{.InputType}}" id="{{$for}}" {{if eq .InputType
"radio"}}name="category_group_{{.CategoryID}}"{{end}} />

View file

@ -35,9 +35,9 @@ func TestRole(t *testing.T) {
assert.Contains(t, html, "--role-color: #a20000;", "role color is set")
assert.Contains(t, html, `type="checkbox"`, "multi has input type=checkbox")
assert.Contains(t, html, fmt.Sprintf("--contrast-color: %s;", utils.RgbToString(utils.AltColor(162, 0, 0))), "contrast color is set")
assert.Contains(t, html, fmt.Sprintf(`id="%s"`, roleInputID(c, r)), "input has ID attr")
assert.Contains(t, html, fmt.Sprintf(`for="%s"`, roleInputID(c, r)), "label has for attr")
assert.NotContains(t, html, fmt.Sprintf(`name="%s"`, roleInputName(c)), "multi has no name attr")
assert.Contains(t, html, fmt.Sprintf(`id="%s"`, utils.RoleInputID(c, r)), "input has ID attr")
assert.Contains(t, html, fmt.Sprintf(`for="%s"`, utils.RoleInputID(c, r)), "label has for attr")
assert.NotContains(t, html, fmt.Sprintf(`name="%s"`, utils.RoleInputName(c)), "multi has no name attr")
// TODO: selected?
c = &fixtures.CategorySingle
@ -45,14 +45,7 @@ func TestRole(t *testing.T) {
html = renderRole(t, c, r, true)
assert.Contains(t, html, `type="radio"`, "single has input type=radio")
assert.Contains(t, html, fmt.Sprintf("--contrast-color: %s;", utils.RgbToString(utils.AltColor(0xff, 0xaa, 0x88))), "contrast color")
assert.Contains(t, html, fmt.Sprintf(`name="%s"`, roleInputName(c)), "single has name attr")
assert.Contains(t, html, fmt.Sprintf(`name="%s"`, utils.RoleInputName(c)), "single has name attr")
}
// TODO: these can probably be string utils that are injected as functions into template
func roleInputID(c *types.Category, r *types.Role) string {
return fmt.Sprintf("category-%s_role-%s", c.ID, r.ID)
}
func roleInputName(c *types.Category) string {
return fmt.Sprintf("category_group_%s", c.ID)
}

View file

@ -5,7 +5,7 @@
<title>{{ .HeadTitle }}</title>
<link rel="preconnect" href="https://fonts.bunny.net" />
<link
href="https://fonts.bunny.net/css?family=atkinson-hyperlegible:400,400i"
href="https://fonts.bunny.net/css?family=atkinson-hyperlegible:400,400i,600,600i"
rel="stylesheet"
/>
<link rel="stylesheet" href="/static/main.css" />

View file

@ -0,0 +1,14 @@
// Package testing provides test helpers that support fiber templates
package testing
import (
"html/template"
"git.sapphic.engineer/roleypoly/v4/templates"
)
var (
Templates = template.Must(template.ParseFS(templates.FS, "*.html")).Funcs(template.FuncMap{
"embed": func() {},
})
)

View file

@ -0,0 +1,7 @@
package testing_test
// func Test(t *testing.T) {
// }
// TODO: test the template tester

View file

@ -7,10 +7,10 @@
<div class="container">
<div class="cat-multi">
{{ template "components/role" .TestRole }} {{ template "components/role"
.TestRole2 }}
.TestRole2 }} {{ template "components/role" .TestRole3 }}
</div>
<div class="cat-single">
{{ template "components/role" .TestRole3 }} {{ template "components/role"
.TestRole4 }}
{{ template "components/role" .TestRole4 }} {{ template "components/role"
.TestRole5 }} {{ template "components/role" .TestRole6 }}
</div>
</div>

View file

@ -40,9 +40,11 @@ func (t *TestingController) TestTemplate(c fiber.Ctx) error {
cat2 := fixtures.Category(fixtures.CategorySingle)
return c.Render("tests/"+which, fiber.Map{
"TestRole": presentation.Role(cat1, &fixtures.RoleWithDarkColor, false),
"TestRole2": presentation.Role(cat1, &fixtures.RoleWithLightColor, true),
"TestRole3": presentation.Role(cat2, &fixtures.RoleWithDarkColor, false),
"TestRole4": presentation.Role(cat2, &fixtures.RoleWithLightColor, true),
"TestRole2": presentation.Role(cat1, &fixtures.RoleWithDarkMediumColor, false),
"TestRole3": presentation.Role(cat1, &fixtures.RoleWithLightColor, true),
"TestRole4": presentation.Role(cat1, &fixtures.RoleWithDarkColor, false),
"TestRole5": presentation.Role(cat2, &fixtures.RoleWithLightColor, true),
"TestRole6": presentation.Role(cat1, &fixtures.RoleWithLightMediumColor, false),
}, "layouts/main")
}

View file

@ -5,17 +5,31 @@ import "git.sapphic.engineer/roleypoly/v4/types"
var (
RoleWithDarkColor = types.Role{
ID: "dark-color",
Name: "role with dark color",
Name: "dark",
Color: 0xa20000,
Permissions: 0,
Position: 10,
}
RoleWithDarkMediumColor = types.Role{
ID: "dark-medium-color",
Name: "dark medium",
Color: 0xeb5e4b,
Permissions: 0,
Position: 11,
}
RoleWithLightMediumColor = types.Role{
ID: "light-medium-color",
Name: "light medium",
Color: 0xfa8373,
Permissions: 0,
Position: 11,
}
RoleWithLightColor = types.Role{
ID: "light-color",
Name: "role with light color",
Name: "light",
Color: 0xffaa88,
Permissions: 0,
Position: 10,
Position: 12,
}
RoleWithoutColor = types.Role{
ID: "without-color",

View file

@ -2,6 +2,7 @@ package utils
import (
"fmt"
"log"
"math"
)
@ -11,11 +12,10 @@ const (
Pblue float64 = 0.114
)
func IsDarkColor(r, g, b uint8) bool {
lum := Luminance(r, g, b)
return lum <= 130
}
var (
DarkAltFallback = []uint8{0x1c, 0x10, 0x10}
LightAltFallback = []uint8{0xf2, 0xef, 0xef}
)
func IntToRgb(color uint32) (uint8, uint8, uint8) {
b := color % 256
@ -30,11 +30,30 @@ func RgbToString(r, g, b uint8) string {
}
func AltColor(r, g, b uint8) (uint8, uint8, uint8) {
l1 := Luminance(r, g, b)
isDark := l1 <= 0.5098
brightnessAmount := -0.6
if IsDarkColor(r, g, b) {
if isDark { // color is dark
brightnessAmount = 0.85
}
return Brighten(r, g, b, brightnessAmount)
r2, g2, b2 := Brighten(r, g, b, brightnessAmount)
l2 := Luminance(r2, g2, b2)
ratio := WCAGRatio(l1, l2)
log.Printf("isDark=%v, ratio: %f, l1(%f)=%s, l2(%f)=%s", isDark, ratio, l1, RgbToString(r, g, b), l2, RgbToString(r2, g2, b2))
if ratio >= 3 {
return r2, g2, b2
}
if isDark {
return LightAltFallback[0], LightAltFallback[1], LightAltFallback[2]
} else {
return DarkAltFallback[0], DarkAltFallback[1], DarkAltFallback[3]
}
}
func Brighten(r, g, b uint8, amount float64) (uint8, uint8, uint8) {
@ -53,18 +72,23 @@ func multiply(i uint8, amount float64) uint8 {
)
}
func sRGB(c uint8) float64 {
cf := float64(c) / 255
if cf <= 0.03928 {
return cf / 12.92
} else {
return math.Pow((cf+0.055)/1.055, 2.4)
}
}
func Luminance(r, g, b uint8) float64 {
rC := Pred * math.Pow(float64(r), 2)
gC := Pgreen * math.Pow(float64(g), 2)
bC := Pblue * math.Pow(float64(b), 2)
rC := Pred * math.Pow(sRGB(r), 2)
gC := Pgreen * math.Pow(sRGB(g), 2)
bC := Pblue * math.Pow(sRGB(b), 2)
return math.Sqrt(rC + gC + bC)
}
func WCAGRatio(l1, l2 float64) float64 {
if l1 < l2 {
return (l1 + 0.05) / (l2 + 0.05)
} else {
return (l2 + 0.05) / (l1 + 0.05)
}
return (math.Max(l1, l2) + 0.05) / (math.Min(l1, l2) + 0.05)
}

View file

@ -8,8 +8,7 @@ import (
)
const (
WCAGAAA float64 = 0.14285714285714285
WCAGAA float64 = 0.25
WCAGAA float64 = 3
)
func TestIntToRgb(t *testing.T) {
@ -19,38 +18,20 @@ func TestIntToRgb(t *testing.T) {
assert.Equal(t, uint8(0x56), b, "blue")
}
func TestIsDarkColor(t *testing.T) {
isDark := utils.IsDarkColor(0, 0, 0)
assert.True(t, isDark)
isLight := utils.IsDarkColor(255, 255, 255)
assert.False(t, isLight)
isQuestionable := utils.IsDarkColor(0x88, 0x88, 0x88)
assert.False(t, isQuestionable)
isReallyQuestionable := utils.IsDarkColor(0x00, 0x88, 0x00)
assert.True(t, isReallyQuestionable)
}
func TestBrighten(t *testing.T) {
r, g, b := utils.Brighten(0, 0, 0, 0.1)
assert.Equal(t, uint8(0x19), r)
assert.Equal(t, uint8(0x19), g)
assert.Equal(t, uint8(0x19), b)
// assert.LessOrEqual(t, WCAGAA, utils.WCAGRatio(
// utils.Luminance(0, 0, 0),
// utils.Luminance(r, g, b),
// ))
r, g, b = utils.Brighten(0x88, 0x88, 0x88, -0.1)
assert.Equal(t, uint8(0x88-0x19-1), r)
assert.Equal(t, uint8(0x88-0x19-1), g)
assert.Equal(t, uint8(0x88-0x19-1), b)
// assert.LessOrEqual(t, WCAGAA, utils.WCAGRatio(
// utils.Luminance(0x88, 0x88, 0x88),
// utils.Luminance(r, g, b),
// ))
assert.GreaterOrEqual(t, utils.WCAGRatio(
utils.Luminance(0x88, 0x88, 0x88),
utils.Luminance(r, g, b),
), WCAGAA)
}
func TestRgbToString(t *testing.T) {
@ -59,12 +40,21 @@ func TestRgbToString(t *testing.T) {
func TestAltColor(t *testing.T) {
r, g, b := utils.AltColor(0xa2, 0xc2, 0x42)
assert.Equal(t, uint8(0x09), r, "red")
assert.Equal(t, uint8(0x29), g, "green")
assert.Equal(t, uint8(0x00), b, "blue")
assert.Equal(t, uint8(0xf2), r, "red")
assert.Equal(t, uint8(0xef), g, "green")
assert.Equal(t, uint8(0xef), b, "blue")
assert.GreaterOrEqual(t, utils.WCAGRatio(
utils.Luminance(0xa2, 0xc2, 0x42),
utils.Luminance(r, g, b),
), WCAGAA)
r, g, b = utils.AltColor(0xa2, 0x15, 0x18)
assert.Equal(t, uint8(0xff), r, "red")
assert.Equal(t, uint8(0xed), g, "green")
assert.Equal(t, uint8(0xf0), b, "blue")
assert.Equal(t, uint8(0xff), r, "red2")
assert.Equal(t, uint8(0xed), g, "green2")
assert.Equal(t, uint8(0xf0), b, "blue2")
assert.GreaterOrEqual(t, utils.WCAGRatio(
utils.Luminance(0xa2, 0x15, 0x18),
utils.Luminance(r, g, b),
), WCAGAA)
}