picker getting closer....

This commit is contained in:
41666 2025-04-07 22:07:29 -07:00
parent 023f1651f8
commit 537b430224
20 changed files with 179 additions and 100 deletions

View file

@ -10,14 +10,8 @@
name = "roleypoly"; name = "roleypoly";
src = ./.; src = ./.;
nativeBuildInputs = with pkgs; [
lightningcss
just
];
preBuild = '' preBuild = ''
lightningcss --minify --bundle --targets '>= 0.25%' static/main.css -o static/main.css~ ${pkgs.lightningcss}/bin/lightningcss --minify --bundle --targets '>= 0.25%' static/main.css -o static/main.css
mv static/main.css~ static/main.css
''; '';
}; };
container = pkgs.dockerTools.buildImage { container = pkgs.dockerTools.buildImage {

View file

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

Before

Width:  |  Height:  |  Size: 325 B

After

Width:  |  Height:  |  Size: 323 B

View file

@ -1,4 +1,10 @@
/* Always first (just in case) */
@import url(styles/palette.css); @import url(styles/palette.css);
/* Sort these */
@import url(styles/category.css);
@import url(styles/nav.css);
@import url(styles/picker.css);
@import url(styles/role.css); @import url(styles/role.css);
* { * {
@ -6,7 +12,17 @@
} }
body { body {
margin: 1rem;
padding: 0;
font-family: "Atkinson Hyperlegible", sans-serif; font-family: "Atkinson Hyperlegible", sans-serif;
background-color: var(--taupe200); background-color: var(--taupe100);
color: var(--taupe600); color: var(--taupe600);
max-width: 960px;
width: 100vw;
}
main {
padding: 4rem 1rem 1rem 1rem;
background-color: var(--taupe300);
box-shadow: 0 0 10px #00000044;
} }

View file

@ -0,0 +1,9 @@
.category {
background-color: var(--taupe200);
padding: 1.5rem;
h3 {
margin: 0 0 0.5rem 0;
padding: 0;
}
}

16
static/styles/nav.css Normal file
View file

@ -0,0 +1,16 @@
nav {
margin: 1rem;
height: 4rem;
display: flex;
background-color: var(--taupe200);
position: fixed;
top: 0;
left: 0;
right: 0;
justify-content: space-between;
align-items: center;
padding: 0 1rem;
max-width: 960px;
width: 100vw;
}

9
static/styles/picker.css Normal file
View file

@ -0,0 +1,9 @@
.picker {
padding-top: 0.5rem;
.categories {
display: flex;
flex-direction: column;
gap: 1rem;
}
}

View file

@ -7,7 +7,7 @@
user-select: none; user-select: none;
padding: 0.369rem; /* this is silly number pls ignore :3 (^noe) */ padding: 0.369rem; /* this is silly number pls ignore :3 (^noe) */
gap: 0.269rem; gap: 0.269rem;
cursor: pointer; /* cursor: pointer; */
transition: all 0.35s ease-in-out; transition: all 0.35s ease-in-out;
input { input {
@ -34,14 +34,16 @@
transition: all 0.35s ease-in-out; transition: all 0.35s ease-in-out;
content: ""; content: "";
mask-image: url(/static/images/check.svg); mask-image: url(/static/images/check.svg);
mask-size: cover; mask-size: 1.216rem 1.216rem;
mask-position: center center; mask-position: center;
mask-border: 2px;
background-color: var(--role-color); background-color: var(--role-color);
opacity: 0; opacity: 0;
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0; bottom: 0;
left: 0; left: -2px;
right: -2px;
width: 1.216rem; width: 1.216rem;
} }
} }
@ -51,6 +53,27 @@
font-weight: 600; font-weight: 600;
} }
/* slightly more specific than the below rule */
&:has(input:disabled) {
cursor: not-allowed;
input {
cursor: not-allowed;
opacity: 1;
&::before {
background-color: var(--role-color);
mask-image: url(/static/images/x.svg);
opacity: 1;
}
&:hover {
background-color: initial !important;
}
}
label {
cursor: not-allowed;
}
}
&:has(input:checked) { &:has(input:checked) {
background-color: var(--role-color); background-color: var(--role-color);

View file

@ -12,5 +12,16 @@ type CategoryTemplateData struct {
} }
func Category(cat *types.Category, roles []*types.Role) CategoryTemplateData { func Category(cat *types.Category, roles []*types.Role) CategoryTemplateData {
return CategoryTemplateData{} rtd := make([]RoleTemplateData, len(roles))
for i, role := range roles {
rtd[i] = Role(cat, role)
}
return CategoryTemplateData{
ID: cat.ID,
Name: cat.Name,
Type: cat.Type,
Roles: rtd,
}
} }

View file

@ -18,7 +18,7 @@ func TestCategoryTemplate(t *testing.T) {
Name: "Multi", Name: "Multi",
Type: types.CategoryMultiple, Type: types.CategoryMultiple,
Roles: []components.RoleTemplateData{ Roles: []components.RoleTemplateData{
components.Role(&fixtures.CategoryMulti, &fixtures.RoleWithDarkColor, true, true), components.Role(&fixtures.CategoryMulti, &fixtures.RoleWithDarkColor),
}, },
} }
html := templatetesting.Template(t, "components/category", c) html := templatetesting.Template(t, "components/category", c)
@ -31,7 +31,7 @@ func TestCategoryTemplate(t *testing.T) {
ID: "456", ID: "456",
Name: "Single", Name: "Single",
Roles: []components.RoleTemplateData{ Roles: []components.RoleTemplateData{
components.Role(&fixtures.CategorySingle, &fixtures.RoleWithDarkColor, true, true), components.Role(&fixtures.CategorySingle, &fixtures.RoleWithDarkColor),
}, },
} }
html = templatetesting.Template(t, "components/category", c) html = templatetesting.Template(t, "components/category", c)

View file

@ -27,7 +27,7 @@ const (
InputRadio InputType = "radio" InputRadio InputType = "radio"
) )
func Role(category *types.Category, role *types.Role, selected bool, safe bool) RoleTemplateData { func Role(category *types.Category, role *types.Role) RoleTemplateData {
inputType := InputCheckbox inputType := InputCheckbox
if category.Type == types.CategorySingle { if category.Type == types.CategorySingle {
inputType = InputRadio inputType = InputRadio
@ -39,10 +39,10 @@ func Role(category *types.Category, role *types.Role, selected bool, safe bool)
ID: role.ID, ID: role.ID,
CategoryID: category.ID, CategoryID: category.ID,
Name: role.Name, Name: role.Name,
Selected: selected, Selected: role.Selected,
InputType: inputType, InputType: inputType,
Colors: colors, Colors: colors,
Safe: safe, Safe: role.Safe,
} }
} }

View file

@ -3,9 +3,10 @@
class="role" class="role"
style="--role-color: {{.Colors.Main}}; --contrast-color: {{.Colors.Alt}};" style="--role-color: {{.Colors.Main}}; --contrast-color: {{.Colors.Alt}};"
data-testid="{{$for}}" data-testid="{{$for}}"
title="{{if not .Safe}}This role is considered unsafe.{{end}}"
> >
<input type="{{.InputType}}" id="{{$for}}" {{if eq .InputType <input type="{{.InputType}}" id="{{$for}}" value="{{$for}}" {{if eq .InputType
"radio"}}name="category_group_{{.CategoryID}}"{{end}} {{if not "radio"}}name="category_group_{{.CategoryID}}"{{end}} {{if not
.Safe}}disabled="disabled"{{end}}/> .Safe}}disabled="disabled"{{end}} {{if .Selected}}checked="checked"{{end}}/>
<label for="{{$for}}">{{.Name}}</label> <label for="{{$for}}">{{.Name}}</label>
</div> </div>

View file

@ -14,7 +14,7 @@ import (
func TestRoleTemplate(t *testing.T) { func TestRoleTemplate(t *testing.T) {
c := &fixtures.CategoryMulti c := &fixtures.CategoryMulti
r := &fixtures.RoleWithDarkColor r := &fixtures.RoleWithDarkColor
data := components.Role(c, r, true, true) data := components.Role(c, r)
html := templatetesting.Template(t, "components/role", data) html := templatetesting.Template(t, "components/role", data)
assert.Contains(t, html, "--role-color: #a20000;", "role color is set") 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, `type="checkbox"`, "multi has input type=checkbox")
@ -22,34 +22,36 @@ func TestRoleTemplate(t *testing.T) {
assert.Contains(t, html, fmt.Sprintf(`id="%s"`, utils.RoleInputID(c.ID, r.ID)), "input has ID attr") assert.Contains(t, html, fmt.Sprintf(`id="%s"`, utils.RoleInputID(c.ID, r.ID)), "input has ID attr")
assert.Contains(t, html, fmt.Sprintf(`for="%s"`, utils.RoleInputID(c.ID, r.ID)), "label has for attr") assert.Contains(t, html, fmt.Sprintf(`for="%s"`, utils.RoleInputID(c.ID, r.ID)), "label has for attr")
assert.NotContains(t, html, fmt.Sprintf(`name="%s"`, utils.RoleInputName(c.ID)), "multi has no name attr") assert.NotContains(t, html, fmt.Sprintf(`name="%s"`, utils.RoleInputName(c.ID)), "multi has no name attr")
assert.Contains(t, html, `checked="checked"`)
// TODO: selected? // TODO: selected?
c = &fixtures.CategorySingle c = &fixtures.CategorySingle
r = &fixtures.RoleWithLightColor r = &fixtures.RoleWithLightColor
data = components.Role(c, r, false, true) data = components.Role(c, r)
html = templatetesting.Template(t, "components/role", data) html = templatetesting.Template(t, "components/role", data)
assert.Contains(t, html, `type="radio"`, "single has input type=radio") 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("--contrast-color: %s;", utils.RgbToString(utils.AltColor(0xff, 0xaa, 0x88))), "contrast color")
assert.Contains(t, html, fmt.Sprintf(`name="%s"`, utils.RoleInputName(c.ID)), "single has name attr") assert.Contains(t, html, fmt.Sprintf(`name="%s"`, utils.RoleInputName(c.ID)), "single has name attr")
assert.NotContains(t, html, `checked="checked"`)
data = components.Role(c, r, false, false) data = components.Role(c, r)
html = templatetesting.Template(t, "components/role", data) html = templatetesting.Template(t, "components/role", data)
assert.Contains(t, html, `disabled="disabled"`) assert.Contains(t, html, `disabled="disabled"`)
} }
func TestRole(t *testing.T) { func TestRole(t *testing.T) {
r := components.Role(&fixtures.CategoryMulti, &fixtures.RoleWithDarkColor, true, true) r := components.Role(&fixtures.CategoryMulti, &fixtures.RoleWithDarkColor)
assert.Equal(t, fixtures.RoleWithDarkColor.ID, r.ID) assert.Equal(t, fixtures.RoleWithDarkColor.ID, r.ID)
assert.Equal(t, fixtures.RoleWithDarkColor.Name, r.Name) assert.Equal(t, fixtures.RoleWithDarkColor.Name, r.Name)
assert.Equal(t, components.InputCheckbox, r.InputType) assert.Equal(t, components.InputCheckbox, r.InputType)
assert.Equal(t, "#a20000", r.Colors.Main) assert.Equal(t, "#a20000", r.Colors.Main)
assert.True(t, r.Selected) assert.True(t, r.Selected)
r = components.Role(&fixtures.CategorySingle, &fixtures.RoleWithDarkColor, false, true) r = components.Role(&fixtures.CategorySingle, &fixtures.RoleWithDarkColor)
assert.Equal(t, components.InputRadio, r.InputType) assert.Equal(t, components.InputRadio, r.InputType)
assert.False(t, r.Selected) assert.False(t, r.Selected)
r = components.Role(&fixtures.CategorySingle, &fixtures.RoleWithLightColor, true, false) r = components.Role(&fixtures.CategorySingle, &fixtures.RoleWithLightColor)
assert.True(t, r.Selected) assert.True(t, r.Selected)
assert.False(t, r.Safe) assert.False(t, r.Safe)
} }

View file

@ -1,16 +1,17 @@
<!DOCTYPE html> <!doctype html>
<html> <html>
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>{{ .HeadTitle }}</title> <title>{{ .HeadTitle }}</title>
<link rel="preconnect" href="https://fonts.bunny.net" /> <link rel="preconnect" href="https://fonts.bunny.net" />
<link <link
href="https://fonts.bunny.net/css?family=atkinson-hyperlegible:400,400i,600,600i" href="https://fonts.bunny.net/css?family=atkinson-hyperlegible:400,400i,700,700i"
rel="stylesheet" rel="stylesheet"
/> />
<link rel="stylesheet" href="/static/main.css" /> <link rel="stylesheet" href="/static/main.css" />
</head> </head>
<body> <body>
{{ template "components/nav" . }} {{embed}} {{ template "components/nav" . }}
<main>{{embed}}</main>
</body> </body>
</html> </html>

View file

@ -1 +1,8 @@
<h1>picker!</h1> <div class="picker">
<h1>Roleypoly</h1>
<div class="categories">
{{ range $_, $category := .Categories }} {{ template "components/category"
$category }} {{ end }}
</div>
</div>

View file

@ -1,28 +0,0 @@
<style>
.container {
margin: 3rem;
}
</style>
<div class="container">
<div>
<h1>Multi</h1>
{{ range $_, $r := .Category1 }} {{ template "components/role" $r }} {{ end
}}
</div>
<div>
<h1>Single</h1>
{{ range $_, $r := .Category2 }} {{ template "components/role" $r }} {{ end
}}
</div>
<div>
<h1>Bad</h1>
{{ range $_, $r := .Category3 }} {{ template "components/role" $r }} {{ end
}}
</div>
<div>
<h1>Hmm</h1>
{{ range $_, $r := .Category4 }} {{ template "components/role" $r }} {{ end
}}
</div>
</div>

View file

@ -31,45 +31,33 @@ func (t *TestingController) Index(c fiber.Ctx) error {
func (t *TestingController) Picker(c fiber.Ctx) error { func (t *TestingController) Picker(c fiber.Ctx) error {
version := c.Params("version", "main") version := c.Params("version", "main")
return c.Render("picker/"+version, fiber.Map{})
allRoles := []*types.Role{
&fixtures.RoleWithDarkColor,
&fixtures.RoleWithDarkMediumColor,
&fixtures.RoleWithLightMediumColor,
&fixtures.RoleWithLightColor,
}
badRoles := []*types.Role{
&fixtures.RoleUnsafe,
&fixtures.RoleUnsafePicked,
}
return c.Render("picker/"+version, fiber.Map{
"Categories": []components.CategoryTemplateData{
components.Category(fixtures.Category(fixtures.CategoryMulti, "multiple"), allRoles),
components.Category(fixtures.Category(fixtures.CategorySingle, "single"), allRoles),
components.Category(fixtures.Category(fixtures.CategoryMulti, "multi w/ unsafe"), badRoles),
components.Category(fixtures.Category(fixtures.CategorySingle, "single w/ unsafe"), badRoles),
},
})
} }
func (t *TestingController) TestTemplate(c fiber.Ctx) error { func (t *TestingController) TestTemplate(c fiber.Ctx) error {
which := c.Params("which") which := c.Params("which")
cat1 := fixtures.Category(fixtures.CategoryMulti)
cat2 := fixtures.Category(fixtures.CategorySingle) return c.Render("tests/"+which, fiber.Map{}, "layouts/main")
cat3 := fixtures.Category(fixtures.CategoryMulti)
cat4 := fixtures.Category(fixtures.CategorySingle)
return c.Render("tests/"+which, fiber.Map{
// multi
"Category1": []components.RoleTemplateData{
components.Role(cat1, &fixtures.RoleWithDarkColor, true, true),
components.Role(cat1, &fixtures.RoleWithDarkMediumColor, true, true),
components.Role(cat1, &fixtures.RoleWithLightMediumColor, true, true),
components.Role(cat1, &fixtures.RoleWithLightColor, true, true),
},
// single
"Category2": []components.RoleTemplateData{
components.Role(cat2, &fixtures.RoleWithDarkColor, true, true),
components.Role(cat2, &fixtures.RoleWithDarkMediumColor, true, true),
components.Role(cat2, &fixtures.RoleWithLightMediumColor, true, true),
components.Role(cat2, &fixtures.RoleWithLightColor, true, true),
},
// unsafe
"Category3": []components.RoleTemplateData{
components.Role(cat3, &fixtures.RoleWithDarkColor, true, false),
components.Role(cat3, &fixtures.RoleWithDarkMediumColor, false, false),
components.Role(cat3, &fixtures.RoleWithLightMediumColor, false, false),
components.Role(cat3, &fixtures.RoleWithLightColor, true, false),
},
// unselected
"Category4": []components.RoleTemplateData{
components.Role(cat4, &fixtures.RoleWithDarkColor, true, true),
components.Role(cat4, &fixtures.RoleWithDarkMediumColor, false, true),
components.Role(cat4, &fixtures.RoleWithLightMediumColor, false, true),
components.Role(cat4, &fixtures.RoleWithLightColor, false, true),
},
}, "layouts/main")
} }
func (t *TestingController) GetMember(c fiber.Ctx) error { func (t *TestingController) GetMember(c fiber.Ctx) error {

View file

@ -10,20 +10,23 @@ import (
var ( var (
CategoryMulti = types.Category{ CategoryMulti = types.Category{
ID: "multi", ID: "multi",
Name: "Roles", Name: "Multi",
Type: types.CategoryMultiple, Type: types.CategoryMultiple,
Roles: []string{RoleWithDarkColor.ID, RoleWithLightColor.ID, RoleWithoutColor.ID}, Roles: []string{RoleWithDarkColor.ID, RoleWithLightColor.ID, RoleWithoutColor.ID},
} }
CategorySingle = types.Category{ CategorySingle = types.Category{
ID: "single", ID: "single",
Name: "Roles", Name: "Single",
Type: types.CategorySingle, Type: types.CategorySingle,
Roles: []string{RoleWithDarkColor.ID, RoleWithLightColor.ID, RoleWithoutColor.ID}, Roles: []string{RoleWithDarkColor.ID, RoleWithLightColor.ID, RoleWithoutColor.ID},
} }
) )
func Category(base types.Category) *types.Category { func Category(base types.Category, name string) *types.Category {
base.ID = fmt.Sprintf("%s-%d", base.ID, rand.Uint32()) base.ID = fmt.Sprintf("%s-%d", base.ID, rand.Uint32())
if name != "" {
base.Name = name
}
return &base return &base
} }

View file

@ -9,6 +9,8 @@ var (
Color: 0xa20000, Color: 0xa20000,
Permissions: 0, Permissions: 0,
Position: 10, Position: 10,
Safe: true,
Selected: true,
} }
RoleWithDarkMediumColor = types.Role{ RoleWithDarkMediumColor = types.Role{
ID: "dark-medium-color", ID: "dark-medium-color",
@ -16,6 +18,7 @@ var (
Color: 0xeb5e4b, Color: 0xeb5e4b,
Permissions: 0, Permissions: 0,
Position: 11, Position: 11,
Safe: true,
} }
RoleWithLightMediumColor = types.Role{ RoleWithLightMediumColor = types.Role{
ID: "light-medium-color", ID: "light-medium-color",
@ -23,6 +26,8 @@ var (
Color: 0xfa8373, Color: 0xfa8373,
Permissions: 0, Permissions: 0,
Position: 11, Position: 11,
Safe: true,
Selected: true,
} }
RoleWithLightColor = types.Role{ RoleWithLightColor = types.Role{
ID: "light-color", ID: "light-color",
@ -30,6 +35,7 @@ var (
Color: 0xffaa88, Color: 0xffaa88,
Permissions: 0, Permissions: 0,
Position: 12, Position: 12,
Safe: true,
} }
RoleWithoutColor = types.Role{ RoleWithoutColor = types.Role{
ID: "without-color", ID: "without-color",
@ -37,6 +43,24 @@ var (
Color: 0x000000, Color: 0x000000,
Permissions: 0, Permissions: 0,
Position: 11, Position: 11,
Safe: true,
}
RoleUnsafe = types.Role{
ID: "unsafe",
Name: "unsafe",
Color: 0x00c200,
Permissions: 0,
Position: 11,
Safe: false,
}
RoleUnsafePicked = types.Role{
ID: "unsafe-picked",
Name: "picked",
Color: 0x0000c2,
Permissions: 0,
Position: 11,
Safe: false,
Selected: true,
} }
//TODO: role with admin (bad) //TODO: role with admin (bad)
//TODO: role with manage roles (bad) //TODO: role with manage roles (bad)

View file

@ -8,4 +8,8 @@ type Role struct {
UnicodeEmoji string `json:"unicode_emoji"` // unused, future? UnicodeEmoji string `json:"unicode_emoji"` // unused, future?
Permissions uint64 `json:"permissions,string"` Permissions uint64 `json:"permissions,string"`
Position uint8 `json:"position"` Position uint8 `json:"position"`
// Not Discord; used internally
Selected bool `json:"-"`
Safe bool `json:"-"`
} }

View file

@ -2,7 +2,6 @@ package utils
import ( import (
"fmt" "fmt"
"log"
"math" "math"
) )
@ -43,7 +42,7 @@ func AltColor(r, g, b uint8) (uint8, uint8, uint8) {
l2 := Luminance(r2, g2, b2) l2 := Luminance(r2, g2, b2)
ratio := WCAGRatio(l1, l2) 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)) // 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 { if ratio >= 3 {
return r2, g2, b2 return r2, g2, b2