feat(01-02): add ui package enums, helpers, base CSS

- tokens.go: semantic token constants
- variants.go: Size/ButtonVariant/ButtonTone/BadgeVariant enums + Normalized*
- helpers.go: mergeAttrs for templ.Attributes
- base.css: resets, :focus-visible ring (no nesting)
This commit is contained in:
Arthur Belleville 2026-05-14 18:45:15 +02:00
parent 5b19a8815c
commit 1ff8e681da
5 changed files with 171 additions and 14 deletions

View file

@ -2,17 +2,4 @@ module backend
go 1.26.1 go 1.26.1
require ( require github.com/a-h/templ v0.3.1020
github.com/a-h/templ v0.3.1020 // indirect
github.com/go-chi/chi/v5 v5.2.5 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.9.2 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/pressly/goose/v3 v3.27.1 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/text v0.36.0 // indirect
)

View file

@ -0,0 +1,28 @@
/* base.css global resets and accessibility floor for the design system.
* Plain CSS only (no @apply, no nesting) so the file is consumable by both
* Tailwind v4 standalone (with @source scanning Go files) and any other
* downstream CSS pipeline.
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
body {
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
color: #0f172a;
background-color: #ffffff;
}
:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}

View file

@ -0,0 +1,16 @@
package ui
import "github.com/a-h/templ"
// mergeAttrs returns a new templ.Attributes containing every key from base,
// with override keys taking precedence on collision. Either input may be nil.
func mergeAttrs(base, override templ.Attributes) templ.Attributes {
out := templ.Attributes{}
for k, v := range base {
out[k] = v
}
for k, v := range override {
out[k] = v
}
return out
}

View file

@ -0,0 +1,21 @@
// Package ui hosts the design-system primitives (Button, Card, Badge) and the
// shared enum surface (Size, ButtonVariant, ButtonTone, BadgeVariant) used by
// every templ page in the application.
//
// Phase 1 ships a minimal subset of the semantic token surface declared here.
// Later phases extend the rules in *.css files rather than restructuring the
// enum/token surface — these constants exist so consumers can refer to tokens
// by name without hard-coding string literals.
package ui
// Semantic token names. Phase 1 ships these for forward compatibility; the
// CSS rules in base.css / button.css / card.css / badge.css do not yet
// dereference these constants. Future phases will route component variants
// through tokens (e.g. ButtonVariantDanger -> TokenDanger).
const (
TokenPrimary string = "primary"
TokenNeutral string = "neutral"
TokenWarning string = "warning"
TokenSuccess string = "success"
TokenDanger string = "danger"
)

View file

@ -0,0 +1,105 @@
package ui
// Size is the canonical component size enum. Phase 1 only renders SizeMD via
// CSS, but every component normalizes Size so future phases can drop in
// `.ui-button-...-sm` / `.ui-button-...-lg` rules without changing call sites.
type Size string
const (
SizeSM Size = "sm"
SizeMD Size = "md"
SizeLG Size = "lg"
)
// ButtonVariant is the semantic-color enum for Button.
type ButtonVariant string
const (
ButtonVariantDefault ButtonVariant = "default"
ButtonVariantNeutral ButtonVariant = "neutral"
ButtonVariantWarning ButtonVariant = "warning"
ButtonVariantSuccess ButtonVariant = "success"
ButtonVariantDanger ButtonVariant = "danger"
)
// ButtonTone is the visual-weight enum for Button (solid vs. soft).
type ButtonTone string
const (
ButtonToneSolid ButtonTone = "solid"
ButtonToneSoft ButtonTone = "soft"
)
// BadgeVariant is the semantic-color enum for Badge.
type BadgeVariant string
const (
BadgeVariantInfo BadgeVariant = "info"
BadgeVariantWarning BadgeVariant = "warning"
BadgeVariantSuccess BadgeVariant = "success"
BadgeVariantDanger BadgeVariant = "danger"
)
// NormalizedSize returns the safe default (SizeMD) for the zero value and any
// value not in the declared set.
func NormalizedSize(size Size) Size {
switch size {
case SizeSM, SizeLG:
return size
default:
return SizeMD
}
}
// NormalizedButtonVariant returns the safe default (ButtonVariantDefault) for
// the zero value and any value not in the declared set.
func NormalizedButtonVariant(variant ButtonVariant) ButtonVariant {
switch variant {
case ButtonVariantNeutral, ButtonVariantWarning, ButtonVariantSuccess, ButtonVariantDanger:
return variant
default:
return ButtonVariantDefault
}
}
// NormalizedButtonTone returns the safe default (ButtonToneSolid) for the zero
// value and any value not in the declared set.
func NormalizedButtonTone(tone ButtonTone) ButtonTone {
switch tone {
case ButtonToneSoft:
return tone
default:
return ButtonToneSolid
}
}
// NormalizedBadgeVariant returns the safe default (BadgeVariantInfo) for the
// zero value and any value not in the declared set.
func NormalizedBadgeVariant(variant BadgeVariant) BadgeVariant {
switch variant {
case BadgeVariantWarning, BadgeVariantSuccess, BadgeVariantDanger:
return variant
default:
return BadgeVariantInfo
}
}
// ButtonClass assembles the deterministic class string for a Button. Inputs
// are normalized before assembly so callers can pass zero values safely.
//
// Example: ButtonClass(ButtonVariantDefault, ButtonToneSolid, SizeMD) ==
// "ui-button ui-button-solid-default-md".
func ButtonClass(variant ButtonVariant, tone ButtonTone, size Size) string {
v := NormalizedButtonVariant(variant)
t := NormalizedButtonTone(tone)
s := NormalizedSize(size)
return "ui-button ui-button-" + string(t) + "-" + string(v) + "-" + string(s)
}
// BadgeClass assembles the deterministic class string for a Badge.
//
// Example: BadgeClass(BadgeVariantInfo) == "ui-badge ui-badge-info".
func BadgeClass(variant BadgeVariant) string {
v := NormalizedBadgeVariant(variant)
return "ui-badge ui-badge-" + string(v)
}