Foundation: tokens, theme, icons, i18n, Button, Spinner, gallery scaffold

This commit is contained in:
ZacharyZhang-NY committed 2026-10-05 11:59:19 -04:00
commit 119f9047e9
95 files changed
+2489

No files matched your search

+28
View File
@@ -0,0 +1,28 @@
package theme
import (
"math"
"github.com/egoist/mygo/ui"
)
// Contrast returns the WCAG contrast ratio of two opaque colors.
func Contrast(a, b ui.Color) float64 {
la, lb := luminance(a), luminance(b)
if la < lb {
la, lb = lb, la
}
return (la + 0.05) / (lb + 0.05)
}
func luminance(c ui.Color) float64 {
return 0.2126*channel(c.R) + 0.7152*channel(c.G) + 0.0722*channel(c.B)
}
func channel(v uint8) float64 {
s := float64(v) / 255
if s <= 0.04045 {
return s / 12.92
}
return math.Pow((s+0.055)/1.055, 2.4)
}
+167
View File
@@ -0,0 +1,167 @@
// Package theme holds MujicaUI's design tokens: colors, type, metrics and motion.
package theme
import (
"time"
"github.com/egoist/mygo/ui"
)
// Tokens are the colors of one appearance.
type Tokens struct {
Dark bool
Background ui.Color
Surface ui.Color
SurfaceHover ui.Color
Text ui.Color
TextMuted ui.Color
Border ui.Color
ControlBorder ui.Color
Accent ui.Color
AccentHover ui.Color
AccentPressed ui.Color
OnAccent ui.Color
AccentText ui.Color
Selection ui.Color
Focus ui.Color
Ornament ui.Color
Success ui.Color
Warning ui.Color
Danger ui.Color
Info ui.Color
DangerFill ui.Color
OnDanger ui.Color
}
// Light returns the ivory appearance.
func Light() Tokens {
return Tokens{
Background: ui.Hex("#F2EBDD"),
Surface: ui.Hex("#FBF7EF"),
SurfaceHover: ui.Hex("#E8DFD0"),
Text: ui.Hex("#211C1D"),
TextMuted: ui.Hex("#6A5B55"),
Border: ui.Hex("#D2C3AF"),
ControlBorder: ui.Hex("#877662"),
Accent: ui.Hex("#7D2034"),
AccentHover: ui.Hex("#651729"),
AccentPressed: ui.Hex("#511020"),
OnAccent: ui.Hex("#FFF8EE"),
AccentText: ui.Hex("#7D2034"),
Selection: ui.Hex("#EAD7D9"),
Focus: ui.Hex("#7D2034"),
Ornament: ui.Hex("#AD9164"),
Success: ui.Hex("#326247"),
Warning: ui.Hex("#805714"),
Danger: ui.Hex("#A12635"),
Info: ui.Hex("#355E80"),
DangerFill: ui.Hex("#A12635"),
OnDanger: ui.Hex("#FFF8EE"),
}
}
// Dark returns the charcoal appearance.
func Dark() Tokens {
return Tokens{
Dark: true,
Background: ui.Hex("#191618"),
Surface: ui.Hex("#242024"),
SurfaceHover: ui.Hex("#342C32"),
Text: ui.Hex("#F2EBDD"),
TextMuted: ui.Hex("#BCAEAA"),
Border: ui.Hex("#51424A"),
ControlBorder: ui.Hex("#968278"),
Accent: ui.Hex("#9C3C53"),
AccentHover: ui.Hex("#AA425C"),
AccentPressed: ui.Hex("#813047"),
OnAccent: ui.Hex("#FFF8EE"),
AccentText: ui.Hex("#E4A0AE"),
Selection: ui.Hex("#492A36"),
Focus: ui.Hex("#E4A0AE"),
Ornament: ui.Hex("#BDA172"),
Success: ui.Hex("#94C6A4"),
Warning: ui.Hex("#DFC080"),
Danger: ui.Hex("#F0A1AA"),
Info: ui.Hex("#9DBDDA"),
DangerFill: ui.Hex("#A12635"),
OnDanger: ui.Hex("#FFF8EE"),
}
}
// HighContrast returns t with secondary text and borders raised for readability.
func (t Tokens) HighContrast() Tokens {
t.TextMuted = t.Text
t.Border = t.ControlBorder
return t
}
// Density sets control heights and the spacing unit.
type Density int
const (
Default Density = iota
Compact
Comfortable
)
// Unit is the base spacing unit in DIPs.
func (d Density) Unit() float32 {
switch d {
case Compact:
return 3
case Comfortable:
return 5
}
return 4
}
// ControlHeight is the minimum height of single-line controls and data rows.
func (d Density) ControlHeight() float32 {
switch d {
case Compact:
return 28
case Comfortable:
return 44
}
return 36
}
func (d Density) String() string {
switch d {
case Compact:
return "Compact"
case Comfortable:
return "Comfortable"
}
return "Default"
}
// Type sizes and families.
const (
H1Size float32 = 28
H2Size float32 = 22
H3Size float32 = 18
BodySize float32 = 14
CaptionSize float32 = 12
LineHeight float32 = 1.5
SerifFont = "Georgia, Songti SC, SimSun, Noto Serif CJK SC, serif"
MonoFont = "monospace"
)
// Shape metrics in DIPs.
const (
ControlRadius float32 = 2
CardRadius float32 = 4
PopupRadius float32 = 6
BorderWidth float32 = 1
FocusWidth float32 = 2
FocusGap float32 = 2
MinHitSize float32 = 28
)
// Motion durations.
const (
StateDuration = 120 * time.Millisecond
PopupDuration = 160 * time.Millisecond
)
+59
View File
@@ -0,0 +1,59 @@
package theme
import (
"testing"
"github.com/egoist/mygo/ui"
)
func TestContrastFloors(t *testing.T) {
for _, tc := range []struct {
name string
tokens Tokens
}{{"light", Light()}, {"dark", Dark()}} {
k := tc.tokens
text := map[string][2]ui.Color{
"Text/Background": {k.Text, k.Background},
"Text/Surface": {k.Text, k.Surface},
"Text/SurfaceHover": {k.Text, k.SurfaceHover},
"Text/Selection": {k.Text, k.Selection},
"TextMuted/Background": {k.TextMuted, k.Background},
"TextMuted/Surface": {k.TextMuted, k.Surface},
"OnAccent/Accent": {k.OnAccent, k.Accent},
"OnAccent/AccentHover": {k.OnAccent, k.AccentHover},
"OnAccent/Pressed": {k.OnAccent, k.AccentPressed},
"OnDanger/DangerFill": {k.OnDanger, k.DangerFill},
"AccentText/Surface": {k.AccentText, k.Surface},
"Success/Surface": {k.Success, k.Surface},
"Warning/Surface": {k.Warning, k.Surface},
"Danger/Surface": {k.Danger, k.Surface},
"Info/Surface": {k.Info, k.Surface},
}
for pair, c := range text {
if r := Contrast(c[0], c[1]); r < 4.5 {
t.Errorf("%s %s = %.2f, want >= 4.5", tc.name, pair, r)
}
}
graphic := map[string][2]ui.Color{
"ControlBorder/Background": {k.ControlBorder, k.Background},
"ControlBorder/Surface": {k.ControlBorder, k.Surface},
"Focus/Background": {k.Focus, k.Background},
}
for pair, c := range graphic {
if r := Contrast(c[0], c[1]); r < 3 {
t.Errorf("%s %s = %.2f, want >= 3", tc.name, pair, r)
}
}
}
}
func TestDensity(t *testing.T) {
for _, tc := range []struct {
d Density
unit, height float32
}{{Compact, 3, 28}, {Default, 4, 36}, {Comfortable, 5, 44}} {
if tc.d.Unit() != tc.unit || tc.d.ControlHeight() != tc.height {
t.Errorf("%v: unit %v height %v", tc.d, tc.d.Unit(), tc.d.ControlHeight())
}
}
}