backend Create the foundational structure for managing design-system CSS with co-located sources and semantic tokens: - Add `cmd/buildstyles` to concatenate ordered CSS sources into a single shipped stylesheet - Define semantic color and effect tokens in `internal/web/ui/base.css` - Move primitive and catalog CSS sources from `static/css/` to co-located locations under `internal/web/ui/` - Update test contract to verify token presence and proper stylesheet generation - Regenerate `static/styles.css` with new semantic token layer and source annotations
12 KiB
Go Backend CSS Sources Per Primitive Implementation Plan
For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Refactor the Go backend design-system CSS so each primitive owns its own co-located source stylesheet while the app and generated catalog still ship a single go-backend/static/styles.css.
Architecture: Keep go-backend/static/styles.css as the only runtime stylesheet, but move CSS source ownership next to the UI code in internal/web/ui and internal/web/ui/catalog. Extend cmd/buildstyles so it concatenates an explicit ordered list of co-located CSS source files into the existing shipped artifact.
Tech Stack: Go, static CSS, just, Go testing, templ-generated HTML consumers
Chunk 1: Build Contract
Task 1: Tighten The Generator Contract Around Co-Located Sources
Files:
-
Modify:
go-backend/cmd/buildstyles/main_test.go -
Modify:
go-backend/internal/web/ui/ui_test.go -
Step 1: Write the failing generator test against explicit source paths
Update go-backend/cmd/buildstyles/main_test.go so the test uses co-located paths instead of a single css directory.
func TestGenerateStylesConcatenatesSourcesInOrder(t *testing.T) {
root := t.TempDir()
basePath := filepath.Join(root, "internal", "web", "ui", "base.css")
buttonPath := filepath.Join(root, "internal", "web", "ui", "button.css")
for path, body := range map[string]string{
basePath: "/* base */\n.base { color: red; }\n",
buttonPath: "/* button */\n.ui-button { color: blue; }\n",
} {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("mkdir for %s: %v", path, err)
}
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
t.Fatalf("write %s: %v", path, err)
}
}
outputPath := filepath.Join(root, "static", "styles.css")
if err := generateStyles(outputPath, []sourceFile{
{Label: "base.css", Path: basePath},
{Label: "button.css", Path: buttonPath},
}); err != nil {
t.Fatalf("generate styles: %v", err)
}
content, err := os.ReadFile(outputPath)
if err != nil {
t.Fatalf("read output: %v", err)
}
got := string(content)
for _, want := range []string{
"/* Source: base.css */",
".base { color: red; }",
"/* Source: button.css */",
".ui-button { color: blue; }",
} {
if !strings.Contains(got, want) {
t.Fatalf("expected %q in %q", want, got)
}
}
if strings.Index(got, "/* Source: base.css */") > strings.Index(got, "/* Source: button.css */") {
t.Fatalf("expected base.css before button.css, got %q", got)
}
}
- Step 2: Keep the built stylesheet assertions focused on the runtime contract
Keep TestSharedSemanticClassesExistInStylesheet in go-backend/internal/web/ui/ui_test.go asserting the generated header and representative selectors:
for _, want := range []string{
`Code generated by cmd/buildstyles`,
`.ui-button-solid.ui-button-default`,
`.ui-badge-warning`,
`.ui-icon-button-solid.ui-icon-button-neutral`,
`.ui-card`,
`.ui-space-x-md`,
`.catalog-page`,
} {
if !strings.Contains(css, want) {
t.Fatalf("expected stylesheet to contain %q", want)
}
}
- Step 3: Run the focused tests to verify the generator test fails first
Run: cd go-backend && go test ./cmd/buildstyles ./internal/web/ui -run 'TestGenerateStylesConcatenatesSourcesInOrder|TestSharedSemanticClassesExistInStylesheet' -count=1
Expected: FAIL in TestGenerateStylesConcatenatesSourcesInOrder until cmd/buildstyles supports explicit co-located source paths.
- Step 4: Commit the updated contract tests
git add go-backend/cmd/buildstyles/main_test.go go-backend/internal/web/ui/ui_test.go
git commit -m "test: define co-located css build contract"
Chunk 2: Generator
Task 2: Update The Stylesheet Generator For Co-Located Inputs
Files:
-
Modify:
go-backend/cmd/buildstyles/main.go -
Modify:
go-backend/justfile -
Step 1: Replace the single-directory model with explicit source file metadata
Update go-backend/cmd/buildstyles/main.go to define each source file by label and path.
type sourceFile struct {
Label string
Path string
}
var sourceFiles = []sourceFile{
{Label: "base.css", Path: filepath.Join("internal", "web", "ui", "base.css")},
{Label: "catalog.css", Path: filepath.Join("internal", "web", "ui", "catalog", "catalog.css")},
{Label: "button.css", Path: filepath.Join("internal", "web", "ui", "button.css")},
{Label: "badge.css", Path: filepath.Join("internal", "web", "ui", "badge.css")},
{Label: "icon-button.css", Path: filepath.Join("internal", "web", "ui", "icon-button.css")},
{Label: "input.css", Path: filepath.Join("internal", "web", "ui", "input.css")},
{Label: "textarea.css", Path: filepath.Join("internal", "web", "ui", "textarea.css")},
{Label: "form-field.css", Path: filepath.Join("internal", "web", "ui", "form-field.css")},
{Label: "modal.css", Path: filepath.Join("internal", "web", "ui", "modal.css")},
{Label: "table.css", Path: filepath.Join("internal", "web", "ui", "table.css")},
{Label: "empty-state.css", Path: filepath.Join("internal", "web", "ui", "empty-state.css")},
{Label: "card.css", Path: filepath.Join("internal", "web", "ui", "card.css")},
{Label: "spacing.css", Path: filepath.Join("internal", "web", "ui", "spacing.css")},
{Label: "app.css", Path: filepath.Join("internal", "web", "ui", "app.css")},
}
- Step 2: Adjust
generateStylesto read from[]sourceFile
Implement the signature and loop like this:
func generateStyles(outputPath string, sources []sourceFile) error {
var buf bytes.Buffer
buf.WriteString("/* Code generated by cmd/buildstyles; DO NOT EDIT. */\n\n")
for i, source := range sources {
body, err := os.ReadFile(source.Path)
if err != nil {
return fmt.Errorf("read %s: %w", source.Path, err)
}
buf.WriteString("/* Source: ")
buf.WriteString(source.Label)
buf.WriteString(" */\n")
buf.Write(bytes.TrimSpace(body))
buf.WriteByte('\n')
if i < len(sources)-1 {
buf.WriteByte('\n')
}
}
if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
return fmt.Errorf("mkdir output dir: %w", err)
}
return os.WriteFile(outputPath, buf.Bytes(), 0o644)
}
- Step 3: Simplify
main()to keep only the output flag
Use:
func main() {
output := flag.String("out", filepath.Join("static", "styles.css"), "output stylesheet path")
flag.Parse()
if err := generateStyles(*output, sourceFiles); err != nil {
fmt.Fprintf(os.Stderr, "build styles: %v\n", err)
os.Exit(1)
}
}
- Step 4: Keep
justbuilding the stylesheet before consumers run
Ensure go-backend/justfile still calls just build-styles before generate, test, build, dev, and run. No change to the generated artifact path.
- Step 5: Run the generator-focused tests
Run: cd go-backend && go test ./cmd/buildstyles -count=1
Expected: PASS.
- Step 6: Commit the generator changes
git add go-backend/cmd/buildstyles/main.go go-backend/cmd/buildstyles/main_test.go go-backend/justfile
git commit -m "feat: build styles from co-located sources"
Chunk 3: Co-Locate CSS Sources
Task 3: Move Primitive And Catalog CSS Beside Their UI Code
Files:
-
Create:
go-backend/internal/web/ui/base.css -
Create:
go-backend/internal/web/ui/button.css -
Create:
go-backend/internal/web/ui/badge.css -
Create:
go-backend/internal/web/ui/icon-button.css -
Create:
go-backend/internal/web/ui/input.css -
Create:
go-backend/internal/web/ui/textarea.css -
Create:
go-backend/internal/web/ui/form-field.css -
Create:
go-backend/internal/web/ui/modal.css -
Create:
go-backend/internal/web/ui/table.css -
Create:
go-backend/internal/web/ui/empty-state.css -
Create:
go-backend/internal/web/ui/card.css -
Create:
go-backend/internal/web/ui/spacing.css -
Create:
go-backend/internal/web/ui/app.css -
Create:
go-backend/internal/web/ui/catalog/catalog.css -
Delete:
go-backend/static/css/base.css -
Delete:
go-backend/static/css/button.css -
Delete:
go-backend/static/css/badge.css -
Delete:
go-backend/static/css/icon-button.css -
Delete:
go-backend/static/css/input.css -
Delete:
go-backend/static/css/textarea.css -
Delete:
go-backend/static/css/form-field.css -
Delete:
go-backend/static/css/modal.css -
Delete:
go-backend/static/css/table.css -
Delete:
go-backend/static/css/empty-state.css -
Delete:
go-backend/static/css/card.css -
Delete:
go-backend/static/css/spacing.css -
Delete:
go-backend/static/css/app.css -
Delete:
go-backend/static/css/catalog.css -
Modify:
go-backend/static/styles.css -
Step 1: Move the base rules into
internal/web/ui/base.css
This file owns:
-
root tokens
-
global reset rules
-
html,body,a,button,input -
.light-only,.dark-only,.visually-hidden -
Step 2: Move primitive selectors into matching co-located files without renaming selectors
Keep each file limited to the selectors owned by that primitive:
-
button.cssowns.ui-button* -
badge.cssowns.ui-badge* -
icon-button.cssowns.ui-icon-button*and.borderless-icon-button* -
input.cssowns.ui-input* -
textarea.cssowns.ui-textarea* -
form-field.cssowns.ui-form-* -
modal.cssowns.ui-modal* -
table.cssowns.ui-table* -
empty-state.cssowns.ui-empty-state* -
card.cssowns.ui-card* -
spacing.cssowns.ui-space-* -
Step 3: Move catalog-only selectors into
internal/web/ui/catalog/catalog.css
This file owns:
-
.catalog-page* -
.catalog-nav* -
.catalog-example* -
.catalog-inline -
.catalog-spacing-row -
.catalog-spacing-column -
.catalog-page-link* -
Step 4: Move remaining UI-layer app selectors into
internal/web/ui/app.css
This file owns non-primitive selectors that still belong to the UI layer, including:
-
auth and home page classes such as
.login-screen,.auth-card-shell,.home-card -
dashboard and overview classes such as
.dashboard-shell,.sidebar-*,.overview-*,.project-*,.tasks-* -
responsive media queries and animation keyframes that support those selectors
-
Step 5: Delete the old
go-backend/static/css/source files once the new copies exist
Do not leave duplicated CSS sources behind.
- Step 6: Rebuild the shipped stylesheet
Run: cd go-backend && go run ./cmd/buildstyles
Expected: go-backend/static/styles.css contains the generated header and /* Source: ... */ sections for the co-located files.
- Step 7: Commit the co-located CSS move
git add go-backend/internal/web/ui/*.css go-backend/internal/web/ui/catalog/catalog.css go-backend/static/styles.css go-backend/static/css
git commit -m "refactor: co-locate ui css sources"
Chunk 4: Verification
Task 4: Verify The Runtime Stylesheet And Consumers
Files:
-
Verify:
go-backend/static/styles.css -
Verify:
go-backend/internal/web/ui/ui_test.go -
Verify:
go-backend/internal/web/ui/catalog/catalog_test.go -
Verify:
go-backend/cmd/designsystem/main_test.go -
Step 1: Run the focused UI and catalog tests
Run: cd go-backend && go test ./internal/web/ui ./internal/web/ui/catalog ./cmd/designsystem -count=1
Expected: PASS.
- Step 2: Run the broader web package verification
Run: cd go-backend && go test ./internal/web/... ./cmd/designsystem ./cmd/buildstyles -count=1
Expected: PASS.
- Step 3: Smoke-check the generated stylesheet headers
Run: cd go-backend && rg -n "Code generated by cmd/buildstyles|Source: button.css|Source: catalog.css|Source: app.css" static/styles.css
Expected: matching lines for the generated header and representative source sections.
- Step 4: Commit the final verification-safe state if anything changed during verification
git add go-backend/static/styles.css
git commit -m "chore: refresh generated stylesheet"