Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
.task/
screens/img/
_output/
ui-hill-climb/rounds/
ui-hill-climb/references/*.png
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ through protanopia, deuteranopia or tritanopia, with every pair the palette is h
|---|---|
| `palette.toml` | `dist/`: `tokens.css` (every role as `--spin-*`), `starlight.css` (the website's `--sl-color-*`), `dashboard.css` (shadcn's HSL variables, light on `:root`, dark on `.dark`, and the dashboard's radius, scrim, dense type scale, heading tracking and motion), `tokens.json`, and `perception.js` and `simulate.svg` for the preview |
| spin's `dashboard/e2e/catalog.spec.ts` | `screens/`: every route, tab, dialog and menu of the app in both themes at 1440 and 390 px, and what each draws that this palette does not have - a colour, a face, a size, a radius, contrast, overflow, a target too small (`task screens SCREENS=<dir>`). The catalog and the audit are committed; the images, hundreds a release, are an archive on a release of this repository that `screens.json` names and Pages unpacks. [See them](https://spin-stack.github.io/brand/screens.html) |
| `assets/mark.png` | `logos/`: the stack of disks, cut from the mark as it was drawn (1024 px on a clear ground): the tile on the brand's navy (512, 256; the touch icon at 180, the favicon at 64 and 32) for a dark ground, and clear (512, 256) for a light one. Each is the drawing reduced; a size it would have to enlarge fails |
| `assets/mark.png` | `logos/`: the stack of disks, cut from the mark as it was drawn (1024 px on a clear ground): the tile on the brand's navy (512, 256; the touch icon at 180, the favicon at 64 and 32) for a dark ground, clear (512, 256) for a light one, and clear with its shadow drawn for the dark page (`mark-dark-*`, 512, 256), where the light one's grey shadow read as a pale ring. Each is the drawing reduced; a size it would have to enlarge fails |

```
go tool task # palette and mark: check, and write dist/ and logos/
Expand Down
53 changes: 36 additions & 17 deletions cmd/mark/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,24 @@ import (
)

// logo is one file logos/ holds: the mark at a size something shows it at, on the navy (a tile)
// or on nothing (clear).
// or on nothing (clear); clear and dark is on nothing too, its shadow drawn for the dark page.
type logo struct {
size int
clear bool
dark bool
}

var sizes = map[string]logo{
"mark-512.png": {512, false}, // the link preview, a dark hero
"mark-256.png": {256, false}, // a dark header
"mark-clear-512.png": {512, true}, // on any ground: a light page, a light hero
"mark-clear-256.png": {256, true}, // a light header
"apple-touch-icon.png": {180, false}, // a phone's home screen
"favicon-64.png": {64, false}, // a browser's tab, whose ground is the browser's
"favicon-32.png": {32, false},
"mark-512.png": {size: 512, clear: false}, // the link preview, a dark hero
"mark-256.png": {size: 256, clear: false}, // a dark header
"mark-clear-512.png": {size: 512, clear: true}, // on any ground: a light page, a light hero
"mark-clear-256.png": {size: 256, clear: true}, // a light header
// On a dark page with no tile: the clear mark's light grey shadow read there as a pale ring.
"mark-dark-512.png": {size: 512, clear: true, dark: true}, // a dark hero
"mark-dark-256.png": {size: 256, clear: true, dark: true}, // a dark header
"apple-touch-icon.png": {size: 180, clear: false}, // a phone's home screen
"favicon-64.png": {size: 64, clear: false}, // a browser's tab, whose ground is the browser's
"favicon-32.png": {size: 32, clear: false},
}

// margin is the square's side over the stack's longer one: a fifth of it around the stack.
Expand All @@ -58,7 +62,7 @@ func run() error {
check := flag.Bool("check", false, "write nothing; fail if logos/ is not what the drawing makes")
flag.Parse()

navy, err := ground(*palette)
navy, page, err := ground(*palette)
if err != nil {
return err
}
Expand All @@ -77,11 +81,15 @@ func run() error {
}

onNavy := shadowOn(drawing, stack, navy)
onDark := shadowOn(drawing, stack, page)

var stale []string
for name, l := range sizes {
src := onNavy
if l.clear {
switch {
case l.dark:
src = onDark
case l.clear:
src = drawing
}
img, err := cut(src, stack, l.size, l.clear, navy)
Expand Down Expand Up @@ -113,19 +121,30 @@ func run() error {
return nil
}

// ground is the palette's brand navy, the tile's ground and the dark page's.
func ground(palette string) (color.RGBA, error) {
var p struct{ Brand map[string]string }
// ground is the palette's brand navy, the tile's ground, and its dark page, the ground of the
// clear mark drawn for a dark page.
func ground(palette string) (navy, page color.RGBA, err error) {
var p struct {
Brand map[string]string
Dark struct{ Page string }
}
if _, err := toml.DecodeFile(palette, &p); err != nil {
return color.RGBA{}, err
return navy, page, err
}
if navy, err = rgbOf(palette, "brand.navy", p.Brand["navy"]); err != nil {
return navy, page, err
}
hex := p.Brand["navy"]
page, err = rgbOf(palette, "dark.page", p.Dark.Page)
return navy, page, err
}

func rgbOf(palette, key, hex string) (color.RGBA, error) {
if len(hex) != 7 || hex[0] != '#' {
return color.RGBA{}, fmt.Errorf("%s: brand.navy %q is no #rrggbb", palette, hex)
return color.RGBA{}, fmt.Errorf("%s: %s %q is no #rrggbb", palette, key, hex)
}
v, err := strconv.ParseUint(hex[1:], 16, 32)
if err != nil {
return color.RGBA{}, fmt.Errorf("%s: brand.navy %q is no #rrggbb", palette, hex)
return color.RGBA{}, fmt.Errorf("%s: %s %q is no #rrggbb", palette, key, hex)
}
return color.RGBA{uint8(v >> 16), uint8(v >> 8), uint8(v), 255}, nil
}
Expand Down
17 changes: 17 additions & 0 deletions cmd/palette/checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ func holds(p palette) ([]result, error) {
hold("text", st+" on "+s, c[st], c[s], text)
}
}
// A state's badge: its colour as text on its colour as a tint, wherever a badge sits.
for _, s := range []string{"page", "surface", "raised"} {
for _, st := range []string{"success", "warning", "error", "accent_text"} {
hold("text", st+" on its badge over "+s, c[st], c[st].over(c[s], p.Tint.Badge), text)
}
}
for _, s := range []string{"page", "surface"} {
hold("text", "label on "+s, c["label"], c[s], text)
sel := c["accent"].over(c[s], p.Tint.Selected)
Expand All @@ -110,6 +116,13 @@ func holds(p palette) ([]result, error) {
}
hold("text", "on_accent on accent", c["on_accent"], c["accent"], text)
hold("text", "white on destructive", white, c["destructive"], text)
st := controlStates(p, c, m.name == "dark")
hold("text", "on_accent on accent under the pointer", c["on_accent"], st.accentHover, text)
hold("text", "on_accent on accent pressed", c["on_accent"], st.accentActive, text)
hold("text", "white on destructive under the pointer", white, st.destructiveHover, text)
hold("text", "white on destructive pressed", white, st.destructiveActive, text)
hold("text", "text on a secondary button", c["text"], st.control, bodyText)
hold("text", "text on a secondary button under the pointer", c["text"], st.controlHover, bodyText)
for _, s := range []string{"page", "raised"} {
hold("non-text", "border_strong on "+s, c["border_strong"], c[s], nonText)
hold("non-text", "accent (the focus ring) on "+s, c["accent"], c[s], nonText)
Expand All @@ -124,6 +137,10 @@ func holds(p palette) ([]result, error) {
apart("state", "selected tint over "+s+" | "+s, c["accent"].over(c[s], p.Tint.Selected), c[s], 0)
apart("hover", "hover tint over "+s+" | "+s, c["accent"].over(c[s], p.Tint.Hover), c[s], 0)
}
apart("hover", "accent under the pointer | accent", st.accentHover, c["accent"], 0)
apart("hover", "destructive under the pointer | destructive", st.destructiveHover, c["destructive"], 0)
apart("hover", "secondary under the pointer | secondary", st.controlHover, st.control, 0)
apart("edge", "secondary button | page", st.control, c["page"], 0)
for _, pair := range [][2]string{
{"success", "error"}, {"success", "warning"}, {"warning", "error"},
{"accent", "success"}, {"accent", "error"},
Expand Down
29 changes: 29 additions & 0 deletions cmd/palette/controls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package main

// The fills a button takes under the pointer and when pressed, derived from the roles so that a
// consumer does not invent them (the dashboard wrote primary/90: the accent at 90% over the page,
// lighter in dark and darker in light, and its white label under 4.5:1 in one of them).
//
// A solid fill - the accent, the destructive - darkens: its label is white, and a darker fill
// keeps it legible in both themes. A quiet fill - a secondary button's - steps one surface up.
type states struct {
accentHover, accentActive rgb
destructiveHover, destructiveActive rgb
control, controlHover rgb
}

func controlStates(p palette, c map[string]rgb, isDark bool) states {
black := rgb{0, 0, 0}
darken := func(fill rgb, n float64) rgb { return black.over(fill, p.Tint.Fill*n) }
s := states{
accentHover: darken(c["accent"], 1), accentActive: darken(c["accent"], 2),
destructiveHover: darken(c["destructive"], 1), destructiveActive: darken(c["destructive"], 2),
// A secondary button is a step off the page: raised in dark, the surface in light, where
// raised read as a disabled grey. Under the pointer, the next step.
control: c["surface"], controlHover: c["raised"],
}
if isDark {
s.control, s.controlHover = c["raised"], c["overlay"]
}
return s
}
14 changes: 12 additions & 2 deletions cmd/palette/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ type palette struct {
Brand map[string]string
Dark mode
Light mode
Tint struct{ Hover, Selected, Scrim float64 }
Type struct {
Tint struct{ Hover, Selected, Scrim, Fill, Badge float64 }
// What lifts a menu or a dialog off what is under it, per theme: a CSS box-shadow.
Elevation struct{ Dark, Light string }
Type struct {
Sans, Mono string
FallbackSans string `toml:"fallback_sans"`
FallbackMono string `toml:"fallback_mono"`
Expand All @@ -44,6 +46,9 @@ type palette struct {
LineHeight float64 `toml:"line_height"`
HeadingWeight int `toml:"heading_weight"`
HeadingTracking float64 `toml:"heading_tracking"`
Heading string
Fluid map[string][2]int
Numeric string
Wordmark string
}
Layout struct {
Expand All @@ -58,6 +63,11 @@ type palette struct {
WidthDetail int `toml:"width_detail"`
WidthWide int `toml:"width_wide"`
}
// A control's geometry: the heights of a button or an input, the icon in it, and the gap
// between an icon and its word.
Controls struct {
Sm, Md, Lg, Icon, Gap int
}
Motion struct {
Fast, Base, Slow, Draw int
Ease string
Expand Down
Loading
Loading