From 1ff8e681da5a3e8a02841698d4a38df0790111ee Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Thu, 14 May 2026 18:45:15 +0200 Subject: [PATCH] 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) --- backend/go.mod | 15 +--- backend/internal/web/ui/base.css | 28 ++++++++ backend/internal/web/ui/helpers.go | 16 +++++ backend/internal/web/ui/tokens.go | 21 ++++++ backend/internal/web/ui/variants.go | 105 ++++++++++++++++++++++++++++ 5 files changed, 171 insertions(+), 14 deletions(-) create mode 100644 backend/internal/web/ui/base.css create mode 100644 backend/internal/web/ui/helpers.go create mode 100644 backend/internal/web/ui/tokens.go create mode 100644 backend/internal/web/ui/variants.go diff --git a/backend/go.mod b/backend/go.mod index 4312cb9..feebdfb 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -2,17 +2,4 @@ module backend go 1.26.1 -require ( - 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 -) +require github.com/a-h/templ v0.3.1020 diff --git a/backend/internal/web/ui/base.css b/backend/internal/web/ui/base.css new file mode 100644 index 0000000..6f17db7 --- /dev/null +++ b/backend/internal/web/ui/base.css @@ -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; +} diff --git a/backend/internal/web/ui/helpers.go b/backend/internal/web/ui/helpers.go new file mode 100644 index 0000000..7afee74 --- /dev/null +++ b/backend/internal/web/ui/helpers.go @@ -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 +} diff --git a/backend/internal/web/ui/tokens.go b/backend/internal/web/ui/tokens.go new file mode 100644 index 0000000..6f111b7 --- /dev/null +++ b/backend/internal/web/ui/tokens.go @@ -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" +) diff --git a/backend/internal/web/ui/variants.go b/backend/internal/web/ui/variants.go new file mode 100644 index 0000000..13cd8bf --- /dev/null +++ b/backend/internal/web/ui/variants.go @@ -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) +}