xtablo-source/docs/superpowers/plans/2026-05-10-go-backend-css-sources-per-primitive.md
Arthur Belleville 8bcf81a3f1
Add co-located CSS sources and semantic token infrastructure to Go
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
2026-05-10 11:47:42 +02:00

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 generateStyles to 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 just building 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.css owns .ui-button*

  • badge.css owns .ui-badge*

  • icon-button.css owns .ui-icon-button* and .borderless-icon-button*

  • input.css owns .ui-input*

  • textarea.css owns .ui-textarea*

  • form-field.css owns .ui-form-*

  • modal.css owns .ui-modal*

  • table.css owns .ui-table*

  • empty-state.css owns .ui-empty-state*

  • card.css owns .ui-card*

  • spacing.css owns .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"