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
This commit is contained in:
Arthur Belleville 2026-05-10 11:47:42 +02:00
parent 0ac8bd0fc9
commit 8bcf81a3f1
No known key found for this signature in database
24 changed files with 4733 additions and 800 deletions

View file

@ -0,0 +1,333 @@
# 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.
```go
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:
```go
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**
```bash
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.
```go
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:
```go
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:
```go
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**
```bash
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**
```bash
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**
```bash
git add go-backend/static/styles.css
git commit -m "chore: refresh generated stylesheet"
```

View file

@ -0,0 +1,329 @@
# Go Backend Semantic CSS Color Tokens 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:** Replace hardcoded color literals across the co-located Go backend CSS sources with semantic tokens defined in `go-backend/internal/web/ui/base.css`.
**Architecture:** Extend `base.css` into the single shared visual token layer for colors, overlays, gradients, and shadows. Then update every co-located CSS file under `internal/web/ui` and `internal/web/ui/catalog` to consume those tokens, keeping the existing selector API and single generated `go-backend/static/styles.css`.
**Tech Stack:** CSS custom properties, Go, `cmd/buildstyles`, `just`, Go `testing`
---
## Chunk 1: Token Contract
## Task 1: Define The Token Contract In Tests
**Files:**
- Modify: `go-backend/internal/web/ui/ui_test.go`
- Modify: `go-backend/cmd/buildstyles/main_test.go`
- [ ] **Step 1: Extend the stylesheet contract test with representative semantic tokens**
Update `TestSharedSemanticClassesExistInStylesheet` in `go-backend/internal/web/ui/ui_test.go` so it asserts token presence in addition to selector presence.
Add checks like:
```go
for _, want := range []string{
`--color-text-primary`,
`--color-surface-default`,
`--color-status-warning-soft-bg`,
`--shadow-surface-md`,
`--overlay-backdrop-default`,
`.ui-button-solid.ui-button-default`,
`.ui-badge-warning`,
`.ui-modal-panel`,
`.catalog-page`,
} {
if !strings.Contains(css, want) {
t.Fatalf("expected stylesheet to contain %q", want)
}
}
```
- [ ] **Step 2: Keep the buildstyles generator test format-stable**
Ensure `go-backend/cmd/buildstyles/main_test.go` still only verifies ordered concatenation and generated headers. Do not add token semantics there; that belongs in the stylesheet contract test.
- [ ] **Step 3: Run the focused contract tests and confirm failure if tokens are not present yet**
Run: `cd go-backend && go test ./cmd/buildstyles ./internal/web/ui -run 'TestGenerateStylesConcatenatesSourcesInOrder|TestSharedSemanticClassesExistInStylesheet' -count=1`
Expected: FAIL in `TestSharedSemanticClassesExistInStylesheet` until `base.css` defines the new semantic tokens and `styles.css` is rebuilt.
- [ ] **Step 4: Commit the failing contract tests**
```bash
git add go-backend/internal/web/ui/ui_test.go go-backend/cmd/buildstyles/main_test.go
git commit -m "test: define semantic css token contract"
```
## Chunk 2: Base Tokens
## Task 2: Introduce Semantic Color And Effect Tokens In `base.css`
**Files:**
- Modify: `go-backend/internal/web/ui/base.css`
- [ ] **Step 1: Group the existing core variables into a clearer semantic structure**
Keep the existing root block, but reorganize it into sections for:
- core neutrals
- borders and surfaces
- brand/action
- semantic statuses
- overlays and shadows
- runtime fallbacks
- [ ] **Step 2: Add semantic text, surface, and border tokens**
Define tokens such as:
```css
--color-text-primary: hsl(0 0% 9%);
--color-text-muted: hsl(0 0% 43.5%);
--color-surface-page: hsl(0 0% 100%);
--color-surface-default: #ffffff;
--color-surface-subtle: hsl(0 0% 96.1%);
--color-border-default: hsl(0 0% 90.9%);
--color-border-strong: #d1d5db;
```
Use the existing values already present in the repo unless there is a documented reason to consolidate.
- [ ] **Step 3: Add semantic brand/action tokens**
Define tokens such as:
```css
--color-brand-primary: #804eec;
--color-brand-primary-hover: #6d28d9;
--color-brand-primary-active: #5b21b6;
--color-focus-ring: rgba(124, 58, 237, 0.2);
--color-brand-foreground: #ffffff;
```
- [ ] **Step 4: Add semantic status token triplets**
For each status family, define soft backgrounds, borders, foregrounds, and stronger action values where the current CSS needs them.
Representative examples:
```css
--color-status-info-soft-bg: #eff6ff;
--color-status-info-soft-border: #bfdbfe;
--color-status-info-foreground: #2563eb;
--color-status-warning-soft-bg: #fff4e2;
--color-status-warning-soft-border: #db9729;
--color-status-warning-foreground: #db9729;
--color-status-warning-strong: #db9729;
--color-status-warning-strong-hover: #c37f12;
--color-status-warning-strong-active: #a9670c;
```
Add equivalent success and danger tokens using the current CSS values.
- [ ] **Step 5: Add shadow, overlay, and fallback tokens**
Define shared tokens used repeatedly across the UI layer, for example:
```css
--shadow-surface-sm: 0 10px 30px rgba(15, 23, 42, 0.05);
--shadow-surface-md: 0 10px 30px rgba(15, 23, 42, 0.06);
--shadow-surface-lg: 0 24px 48px rgba(15, 23, 42, 0.18);
--overlay-backdrop-default: rgba(17, 24, 39, 0.52);
--color-project-fallback: #3b82f6;
```
- [ ] **Step 6: Run the focused contract test**
Run: `cd go-backend && go test ./internal/web/ui -run TestSharedSemanticClassesExistInStylesheet -count=1`
Expected: still FAIL until the rest of the CSS files consume the tokens and `styles.css` is regenerated.
- [ ] **Step 7: Commit the token vocabulary**
```bash
git add go-backend/internal/web/ui/base.css go-backend/internal/web/ui/ui_test.go
git commit -m "feat: add semantic css token vocabulary"
```
## Chunk 3: Primitive And Catalog Adoption
## Task 3: Replace Hardcoded Literals In Primitive And Catalog CSS
**Files:**
- Modify: `go-backend/internal/web/ui/button.css`
- Modify: `go-backend/internal/web/ui/badge.css`
- Modify: `go-backend/internal/web/ui/icon-button.css`
- Modify: `go-backend/internal/web/ui/input.css`
- Modify: `go-backend/internal/web/ui/textarea.css`
- Modify: `go-backend/internal/web/ui/form-field.css`
- Modify: `go-backend/internal/web/ui/modal.css`
- Modify: `go-backend/internal/web/ui/card.css`
- Modify: `go-backend/internal/web/ui/empty-state.css`
- Modify: `go-backend/internal/web/ui/catalog/catalog.css`
- [ ] **Step 1: Convert button variants to semantic tokens**
In `button.css`, replace all hardcoded foreground, background, hover, and active values with tokens from `base.css`.
Examples:
```css
.ui-button:focus-visible {
box-shadow: 0 0 0 3px var(--color-focus-ring);
}
.ui-button-solid.ui-button-default {
background: var(--color-brand-primary);
color: var(--color-brand-foreground);
}
```
- [ ] **Step 2: Convert badges to semantic status tokens**
In `badge.css`, replace each family with status tokens:
```css
.ui-badge-warning {
background: var(--color-status-warning-soft-bg);
border-color: var(--color-status-warning-soft-border);
color: var(--color-status-warning-foreground);
}
```
- [ ] **Step 3: Convert shared control styles**
In `input.css`, `textarea.css`, and `form-field.css`, replace hardcoded text, placeholder, border, and focus values with semantic tokens.
- [ ] **Step 4: Convert modal, card, and empty-state files**
Use semantic surface, border, text, shadow, and overlay tokens in:
- `modal.css`
- `card.css`
- `empty-state.css`
- [ ] **Step 5: Convert icon button and catalog styling**
Replace hardcoded muted text, hover backgrounds, snippet backgrounds, and accent colors in:
- `icon-button.css`
- `catalog/catalog.css`
- [ ] **Step 6: Rebuild the generated stylesheet**
Run: `cd go-backend && go run ./cmd/buildstyles`
Expected: `go-backend/static/styles.css` now contains the semantic token names and no longer relies on hardcoded values in these source files.
- [ ] **Step 7: Run focused UI and catalog verification**
Run: `cd go-backend && go test ./internal/web/ui ./internal/web/ui/catalog ./cmd/buildstyles -count=1`
Expected: PASS.
- [ ] **Step 8: Commit primitive/catalog adoption**
```bash
git add go-backend/internal/web/ui/*.css go-backend/internal/web/ui/catalog/catalog.css go-backend/static/styles.css
git commit -m "refactor: replace primitive css colors with semantic tokens"
```
## Chunk 4: App Layer Adoption
## Task 4: Replace Hardcoded Literals In `app.css`
**Files:**
- Modify: `go-backend/internal/web/ui/app.css`
- Modify: `go-backend/internal/web/ui/base.css`
- [ ] **Step 1: Identify repeated app-only literals and map them to existing tokens first**
Before adding any new token, check whether an existing semantic token already fits. Reuse it unless the app needs a distinct visual meaning.
- [ ] **Step 2: Add only the additional semantic tokens that `app.css` truly needs**
Examples likely needed:
- app panel surface variants
- sidebar/background translucency variants
- stronger app-specific shadow tokens
- gradient stop tokens
- project accent fallback tokens
Do not add raw-value-named variables.
- [ ] **Step 3: Replace hardcoded app colors section by section**
Update:
- auth/login surfaces and gradients
- sidebar surfaces, text, borders, shadows
- overview/project/task status styling
- not-found and hero surfaces
- task widgets and quick-action cards
Preserve the current rendered appearance.
- [ ] **Step 4: Keep runtime variables semantic**
Update patterns like:
```css
background: var(--project-color, var(--color-project-fallback));
```
instead of falling back to raw hex values.
- [ ] **Step 5: Rebuild the generated stylesheet**
Run: `cd go-backend && go run ./cmd/buildstyles`
Expected: `static/styles.css` reflects the tokenized app layer.
- [ ] **Step 6: Run broader verification**
Run: `cd go-backend && go test ./internal/web/... ./cmd/designsystem ./cmd/buildstyles -count=1`
Expected: PASS.
- [ ] **Step 7: Commit app-layer token adoption**
```bash
git add go-backend/internal/web/ui/app.css go-backend/internal/web/ui/base.css go-backend/static/styles.css
git commit -m "refactor: replace app css colors with semantic tokens"
```
## Chunk 5: Final Audit
## Task 5: Prove The Refactor Removed Hardcoded Color Drift
**Files:**
- Verify: `go-backend/internal/web/ui/*.css`
- Verify: `go-backend/internal/web/ui/catalog/catalog.css`
- Verify: `go-backend/static/styles.css`
- [ ] **Step 1: Search for remaining hardcoded colors in the co-located CSS sources**
Run: `cd go-backend && rg -n "#[0-9a-fA-F]{3,8}|rgba\\(|hsl\\(" internal/web/ui/*.css internal/web/ui/catalog/*.css`
Expected: only intentional token definitions remain in `base.css`, plus any explicitly justified one-off literals that were consciously retained.
- [ ] **Step 2: Search the generated stylesheet for representative token names**
Run: `cd go-backend && rg -n -- '--color-text-primary|--color-surface-default|--color-status-warning-soft-bg|--shadow-surface-md|--overlay-backdrop-default' static/styles.css`
Expected: matching lines exist.
- [ ] **Step 3: Re-run the full verification set fresh**
Run: `cd go-backend && go test ./internal/web/... ./cmd/designsystem ./cmd/buildstyles -count=1`
Expected: PASS.
- [ ] **Step 4: Commit any final cleanup needed after audit**
```bash
git add go-backend/internal/web/ui/*.css go-backend/internal/web/ui/catalog/catalog.css go-backend/static/styles.css go-backend/internal/web/ui/ui_test.go
git commit -m "chore: finalize semantic css token refactor"
```

View file

@ -0,0 +1,184 @@
# Go Backend Semantic CSS Color Tokens Design
**Date:** 2026-05-10
**Goal**
Replace hardcoded colors across the co-located Go backend CSS sources with a semantic token layer defined in `go-backend/internal/web/ui/base.css`.
**Chosen Approach**
Introduce a structured set of semantic CSS custom properties in `base.css`, then update every co-located CSS file to consume those variables instead of raw hex, rgba, or hsl color literals. This keeps the styling readable, centralizes visual decisions, and prevents component files like `button.css` from hardcoding their own palette values.
**Scope**
- Add semantic color, surface, border, shadow, and overlay tokens to `go-backend/internal/web/ui/base.css`.
- Replace hardcoded color literals in all co-located CSS source files:
- `go-backend/internal/web/ui/*.css`
- `go-backend/internal/web/ui/catalog/catalog.css`
- Keep the existing single shipped output: `go-backend/static/styles.css`.
- Preserve the current visual appearance as closely as possible while changing only the source of truth for values.
**Out Of Scope**
- Changing component HTML contracts
- Redesigning the visual theme
- Adding dark mode
- Replacing dynamic runtime variables such as `--project-color`
- Tokenizing spacing, radii, typography, or layout values in this slice unless directly necessary for a color/shadow token
**Architecture**
The CSS should have two layers of visual definition:
1. **Semantic token layer in `base.css`**
- neutral text, surface, and border tokens
- brand and action tokens
- semantic status tokens for info, warning, success, and danger
- overlay, gradient, and shadow tokens
2. **Component and app usage layer**
- every other CSS file references tokens from `base.css`
- component files stop introducing their own color literals
This preserves the current compiled stylesheet shape while moving visual decisions into one place.
**Token Strategy**
Use semantic names, not raw-value names.
Good examples:
- `--color-text-primary`
- `--color-text-muted`
- `--color-surface-default`
- `--color-surface-subtle`
- `--color-border-default`
- `--color-brand-primary`
- `--color-brand-primary-hover`
- `--color-status-success-soft-bg`
- `--color-status-danger-strong`
- `--shadow-surface-md`
- `--overlay-backdrop-default`
Bad examples:
- `--color-7c3aed`
- `--purple-500`
- `--gray-200`
The token names should describe intent so components remain readable when using them.
**Token Categories**
Recommended categories in `base.css`:
1. **Core neutrals**
- page background
- primary text
- muted text
- default border
- subtle surface
- raised surface
2. **Brand / action**
- primary action base, hover, active
- accent / highlight
- focus ring
3. **Semantic statuses**
- info background, border, foreground
- warning background, border, foreground
- success background, border, foreground
- danger background, border, foreground
- stronger action-oriented variants where needed for buttons
4. **Effects**
- shadows used by cards, modals, panels, and app surfaces
- overlay/backdrop colors
- shared gradient stops if reused in multiple places
5. **Runtime-aware fallbacks**
- values that back `var(--project-color, ...)` should use semantic fallbacks such as `var(--project-color, var(--color-project-fallback))`
**Responsibility Boundaries**
`base.css`
- owns all reusable visual tokens
- remains the only file allowed to define shared palette decisions
Other CSS files
- consume tokens only
- may still define non-color properties such as layout and spacing
- should not introduce new raw color literals unless there is a strong, deliberate reason
`app.css`
- remains app-specific in selector ownership
- still uses the same shared semantic token layer rather than inventing app-only raw values everywhere
**Migration Strategy**
Refactor in one coherent slice:
- define the token vocabulary in `base.css`
- replace hardcoded literals in primitive CSS files
- replace hardcoded literals in `catalog.css`
- replace hardcoded literals in `app.css`
- regenerate `go-backend/static/styles.css`
- verify the generated stylesheet still exposes the expected selectors and that tests still pass
The work should preserve current behavior. This is a source-of-truth refactor, not a visual refresh.
**Testing**
Verification should cover:
- existing UI tests still passing
- catalog tests still passing
- buildstyles tests still passing
- representative token names present in the generated stylesheet
- representative component selectors still present in the generated stylesheet
Representative generated-token checks:
- `--color-text-primary`
- `--color-surface-default`
- `--color-status-warning-soft-bg`
- `--shadow-surface-md`
- `--overlay-backdrop-default`
Representative selector checks:
- `.ui-button-solid.ui-button-default`
- `.ui-badge-warning`
- `.ui-modal-panel`
- `.catalog-page`
**Risks**
Primary risks:
- introducing token names that are too literal and not reusable
- accidentally changing appearance while replacing values
- leaving scattered raw colors behind in `app.css`
- over-tokenizing one-off values that do not represent shared decisions
Mitigation:
- define token categories before replacing usages
- preserve current values exactly unless consolidation is clearly correct
- use semantic names tied to UI intent
- verify the built stylesheet and tests after the refactor
**Success Criteria**
This work is complete when:
- `base.css` defines a semantic token layer for shared colors and effects
- component and app CSS files use those tokens instead of raw color literals
- the generated `go-backend/static/styles.css` is rebuilt successfully
- the app still ships one stylesheet
- tests still pass

View file

@ -0,0 +1,65 @@
package main
import (
"bytes"
"flag"
"fmt"
"os"
"path/filepath"
)
var sourceOrder = []string{
filepath.Join("internal", "web", "ui", "base.css"),
filepath.Join("internal", "web", "ui", "catalog", "catalog.css"),
filepath.Join("internal", "web", "ui", "button.css"),
filepath.Join("internal", "web", "ui", "badge.css"),
filepath.Join("internal", "web", "ui", "icon-button.css"),
filepath.Join("internal", "web", "ui", "input.css"),
filepath.Join("internal", "web", "ui", "textarea.css"),
filepath.Join("internal", "web", "ui", "form-field.css"),
filepath.Join("internal", "web", "ui", "modal.css"),
filepath.Join("internal", "web", "ui", "table.css"),
filepath.Join("internal", "web", "ui", "empty-state.css"),
filepath.Join("internal", "web", "ui", "card.css"),
filepath.Join("internal", "web", "ui", "spacing.css"),
filepath.Join("internal", "web", "ui", "app.css"),
}
func main() {
output := flag.String("out", filepath.Join("static", "styles.css"), "output stylesheet path")
flag.Parse()
if err := generateStyles(sourceOrder, *output); err != nil {
fmt.Fprintf(os.Stderr, "build styles: %v\n", err)
os.Exit(1)
}
}
func generateStyles(sourcePaths []string, outputPath string) error {
var buf bytes.Buffer
buf.WriteString("/* Code generated by cmd/buildstyles; DO NOT EDIT. */\n\n")
for i, path := range sourcePaths {
body, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read %s: %w", path, err)
}
buf.WriteString("/* Source: ")
buf.WriteString(filepath.ToSlash(path))
buf.WriteString(" */\n")
buf.Write(bytes.TrimSpace(body))
buf.WriteByte('\n')
if i < len(sourcePaths)-1 {
buf.WriteByte('\n')
}
}
if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
return fmt.Errorf("mkdir output dir: %w", err)
}
if err := os.WriteFile(outputPath, buf.Bytes(), 0o644); err != nil {
return fmt.Errorf("write %s: %w", outputPath, err)
}
return nil
}

View file

@ -0,0 +1,63 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestGenerateStylesConcatenatesSourcesInOrder(t *testing.T) {
root := t.TempDir()
uiDir := filepath.Join(root, "internal", "web", "ui")
catalogDir := filepath.Join(uiDir, "catalog")
if err := os.MkdirAll(catalogDir, 0o755); err != nil {
t.Fatalf("mkdir css dirs: %v", err)
}
sources := map[string]string{
filepath.Join(uiDir, "base.css"): "/* base */\n.base { color: red; }\n",
filepath.Join(catalogDir, "catalog.css"): "/* catalog */\n.catalog-page { color: green; }\n",
filepath.Join(uiDir, "button.css"): "/* button */\n.ui-button { color: blue; }\n",
}
for path, body := range sources {
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
t.Fatalf("write %s: %v", path, err)
}
}
outputPath := filepath.Join(root, "static", "styles.css")
sourcePaths := []string{
filepath.Join(uiDir, "base.css"),
filepath.Join(catalogDir, "catalog.css"),
filepath.Join(uiDir, "button.css"),
}
if err := generateStyles(sourcePaths, outputPath); 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{
"/* base */",
".base { color: red; }",
"/* catalog */",
".catalog-page { color: green; }",
"/* button */",
".ui-button { color: blue; }",
} {
if !strings.Contains(got, want) {
t.Fatalf("expected %q in %q", want, got)
}
}
if strings.Index(got, "/* base */") > strings.Index(got, "/* catalog */") {
t.Fatalf("expected base.css before catalog.css, got %q", got)
}
if strings.Index(got, "/* catalog */") > strings.Index(got, "/* button */") {
t.Fatalf("expected catalog.css before button.css, got %q", got)
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,33 @@
.ui-badge {
border: 1px solid transparent;
border-radius: 999px;
display: inline-flex;
font-size: 0.75rem;
font-weight: 600;
line-height: 1.2;
padding: 0.3rem 0.75rem;
}
.ui-badge-info {
background: var(--color-status-info-soft-bg);
border-color: var(--color-status-info-soft-border);
color: var(--color-status-info-foreground);
}
.ui-badge-warning {
background: var(--color-status-warning-soft-bg);
border-color: var(--color-status-warning-soft-border);
color: var(--color-status-warning-foreground);
}
.ui-badge-success {
background: var(--color-status-success-soft-bg);
border-color: var(--color-status-success-soft-border);
color: var(--color-status-success-foreground);
}
.ui-badge-danger {
background: var(--color-status-danger-soft-bg);
border-color: var(--color-status-danger-soft-border);
color: var(--color-status-danger-foreground);
}

View file

@ -0,0 +1,223 @@
:root {
/* Text */
--color-text-primary: hsl(0 0% 9%);
--color-text-secondary: #475467;
--color-text-muted: hsl(0 0% 43.5%);
--color-text-faint: #9ca3af;
--color-text-inverse: #ffffff;
--color-text-brand: #804eec;
--color-text-brand-hover: #6f3fd4;
--color-text-brand-strong: #7c3aed;
--color-text-brand-accent: #7f56d9;
--color-text-heading-alt: #1f2937;
--color-text-body-subtle: #374151;
--color-text-google: #1f1f1f;
--color-text-overlay: #344054;
--color-text-disabled: #667085;
/* Surfaces */
--color-surface-page: hsl(0 0% 100%);
--color-surface-default: #ffffff;
--color-surface-card: rgba(255, 255, 255, 0.8);
--color-surface-subtle: hsl(0 0% 96.1%);
--color-surface-muted: #f3f4f6;
--color-surface-muted-hover: #e5e7eb;
--color-surface-muted-active: #d1d5db;
--color-surface-muted-inverse: #111827;
--color-surface-elevated: rgba(255, 255, 255, 0.92);
--color-surface-elevated-strong: rgba(255, 255, 255, 0.95);
--color-surface-elevated-soft: rgba(255, 255, 255, 0.9);
--color-surface-overlay: rgba(255, 255, 255, 0.88);
--color-surface-overlay-strong: rgba(255, 255, 255, 0.96);
--color-surface-brand-soft: #ede9fe;
--color-surface-brand-soft-hover: #ddd6fe;
--color-surface-brand-soft-active: #c4b5fd;
--color-surface-brand-muted: #f4f3ff;
--color-surface-neutral-hover: rgba(249, 250, 251, 0.9);
--color-surface-page-tint: #f8f7ff;
--color-surface-page-tint-alt: #f4f7fb;
/* Borders */
--color-border-default: hsl(0 0% 90.9%);
--color-border-strong: #d1d5db;
--color-border-muted: #e5e7eb;
--color-border-subtle: #d0d5dd;
--color-border-google: #747775;
--color-border-panel: rgba(30, 27, 46, 0.08);
--color-border-panel-muted: rgba(107, 114, 128, 0.22);
--color-border-panel-strong: rgba(107, 114, 128, 0.35);
--color-border-overlay: rgba(148, 163, 184, 0.22);
--color-border-overlay-strong: rgba(148, 163, 184, 0.3);
/* Brand and focus */
--color-brand-ink: #1e1b2e;
--color-brand-primary: #804eec;
--color-brand-primary-hover: #6d28d9;
--color-brand-primary-active: #5b21b6;
--color-brand-secondary: #a855f7;
--color-brand-accent: #3b82f6;
--color-focus-ring: rgba(124, 58, 237, 0.2);
--color-focus-ring-strong: rgba(139, 92, 246, 0.16);
--color-ring-subtle: rgba(30, 27, 46, 0.35);
/* Status: info */
--color-status-info-soft-bg: #eff6ff;
--color-status-info-soft-border: #bfdbfe;
--color-status-info-foreground: #2563eb;
/* Status: warning */
--color-status-warning-soft-bg: #fff4e2;
--color-status-warning-soft-border: #db9729;
--color-status-warning-foreground: #db9729;
--color-status-warning-strong: #db9729;
--color-status-warning-strong-hover: #c37f12;
--color-status-warning-strong-active: #a9670c;
--color-status-warning-strong-foreground: #ffffff;
--color-status-warning-soft-foreground-strong: #b86e00;
--color-status-warning-soft-bg-hover: #fee6b7;
--color-status-warning-soft-bg-active: #fdd58e;
--color-status-warning-emphasis-bg: #fffbeb;
--color-status-warning-emphasis-border: #fde68a;
--color-status-warning-emphasis-foreground: #ca8a04;
/* Status: success */
--color-status-success-soft-bg: #ecfdf3;
--color-status-success-soft-border: #bbf7d0;
--color-status-success-foreground: #16a34a;
--color-status-success-strong: #16a34a;
--color-status-success-strong-hover: #15803d;
--color-status-success-strong-active: #166534;
--color-status-success-strong-foreground: #ffffff;
--color-status-success-soft-foreground-strong: #15803d;
--color-status-success-soft-bg-hover: #d1fadf;
--color-status-success-soft-bg-active: #a6f4c5;
--color-status-success-banner-bg: hsl(143 85% 96%);
--color-status-success-banner-border: hsl(145 92% 87%);
--color-status-success-banner-foreground: hsl(140 100% 27%);
/* Status: danger */
--color-status-danger-soft-bg: #fef2f2;
--color-status-danger-soft-bg-alt: #fef3f2;
--color-status-danger-soft-border: #fecaca;
--color-status-danger-foreground: #dc2626;
--color-status-danger-strong: #dc2626;
--color-status-danger-strong-hover: #b91c1c;
--color-status-danger-strong-active: #991b1b;
--color-status-danger-strong-foreground: #ffffff;
--color-status-danger-soft-foreground-strong: #b42318;
--color-status-danger-soft-bg-hover: #fee4e2;
--color-status-danger-soft-bg-active: #fecdca;
--color-status-danger-icon-hover: #ef4444;
--color-status-danger-banner-bg: hsl(359 100% 97%);
--color-status-danger-banner-border: hsl(359 100% 94%);
--color-status-danger-banner-foreground: hsl(360 100% 45%);
/* Effects */
--overlay-backdrop-default: rgba(17, 24, 39, 0.52);
--overlay-dark-soft: rgba(30, 27, 46, 0.05);
--overlay-dark-soft-alt: rgba(30, 27, 46, 0.06);
--overlay-dark-border: rgba(30, 27, 46, 0.08);
--overlay-dark-strong: rgba(30, 27, 46, 0.14);
--overlay-brand-soft: rgba(124, 58, 237, 0.1);
--overlay-brand-soft-strong: rgba(124, 58, 237, 0.14);
--overlay-brand-muted: rgba(128, 78, 236, 0.08);
--overlay-brand-faint: rgba(128, 78, 236, 0.04);
--overlay-brand-glow: rgba(128, 78, 236, 0.1);
--overlay-google-state: #303030;
--shadow-auth-card: 0 20px 45px rgba(0, 0, 0, 0.1);
--shadow-surface-sm: 0 10px 30px rgba(15, 23, 42, 0.05);
--shadow-surface-md: 0 10px 30px rgba(15, 23, 42, 0.06);
--shadow-surface-hover: 0 12px 30px rgba(15, 23, 42, 0.08);
--shadow-surface-lg: 0 24px 48px rgba(15, 23, 42, 0.18);
--shadow-surface-xl: 0 32px 70px rgba(15, 23, 42, 0.12);
--shadow-sidebar: 20px 0 45px rgba(30, 27, 46, 0.06);
--shadow-floating-control: 0 10px 24px rgba(30, 27, 46, 0.14);
--shadow-google-button:
0 1px 2px 0 rgba(60, 64, 67, 0.3),
0 1px 3px 1px rgba(60, 64, 67, 0.15);
--shadow-brand-action: 0 18px 35px rgba(124, 58, 237, 0.25);
--gradient-shell:
linear-gradient(135deg, var(--overlay-brand-muted), transparent 30%),
linear-gradient(160deg, var(--overlay-dark-soft), transparent 42%),
linear-gradient(to bottom right, var(--overlay-dark-border), var(--color-surface-page), var(--overlay-brand-faint));
--gradient-card-glow:
linear-gradient(to bottom right, rgba(30, 27, 46, 0.1), var(--overlay-dark-soft), var(--overlay-brand-glow));
--gradient-overview-badge:
linear-gradient(to right, var(--color-brand-secondary), var(--color-brand-accent));
--gradient-app-surface:
linear-gradient(180deg, var(--color-surface-overlay-strong) 0%, var(--color-surface-default) 100%);
--gradient-not-found-bg:
radial-gradient(circle at top, var(--overlay-brand-soft-strong), transparent 35%),
linear-gradient(180deg, var(--color-surface-page-tint) 0%, var(--color-surface-page-tint-alt) 100%);
--gradient-not-found-primary:
linear-gradient(135deg, var(--color-text-brand-strong) 0%, var(--color-status-info-foreground) 100%);
/* Runtime fallbacks */
--color-project-fallback: #3b82f6;
--color-project-accent-purple: #a855f7;
--color-project-accent-red: #ef4444;
/* Legacy aliases */
--background: var(--color-surface-page);
--foreground: var(--color-text-primary);
--muted-foreground: var(--color-text-muted);
--border: var(--color-border-default);
--input: var(--color-border-default);
--card: var(--color-surface-card);
--accent: var(--color-surface-subtle);
--primary: var(--color-brand-ink);
--primary-foreground: var(--color-text-inverse);
--secondary: var(--color-brand-primary);
--ring: var(--color-ring-subtle);
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
min-height: 100%;
}
body {
background: var(--background);
color: var(--foreground);
font-family:
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
button,
input {
font: inherit;
}
.light-only {
display: block;
}
.dark-only {
display: none;
}
.visually-hidden {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}

View file

@ -0,0 +1,162 @@
.ui-button {
align-items: center;
border: 0;
border-radius: 0rem;
cursor: pointer;
display: inline-flex;
font-weight: 600;
gap: 0.5rem;
justify-content: center;
line-height: 1;
min-height: 44px;
text-decoration: none;
transition:
background-color 0.2s ease,
color 0.2s ease,
box-shadow 0.2s ease,
opacity 0.2s ease;
}
.ui-button-icon,
.ui-button-icon svg {
height: 1rem;
width: 1rem;
}
.ui-button:focus-visible {
box-shadow: 0 0 0 3px var(--color-focus-ring);
outline: none;
}
.ui-button-sm {
font-size: 0.875rem;
min-height: 40px;
padding: 0.625rem 0.9rem;
}
.ui-button-md {
font-size: 0.95rem;
padding: 0.75rem 1.1rem;
}
.ui-button-lg {
font-size: 1rem;
padding: 0.82rem 1.15rem;
}
.ui-button-solid.ui-button-default {
background: var(--color-brand-primary);
color: var(--color-brand-foreground, var(--color-text-inverse));
}
.ui-button-solid.ui-button-default:hover {
background: var(--color-brand-primary-hover);
}
.ui-button-solid.ui-button-default:active {
background: var(--color-brand-primary-active);
}
.ui-button-solid.ui-button-neutral {
background: var(--color-surface-muted);
color: var(--color-text-primary);
}
.ui-button-solid.ui-button-neutral:hover {
background: var(--color-surface-muted-hover);
}
.ui-button-solid.ui-button-neutral:active {
background: var(--color-surface-muted-active);
}
.ui-button-solid.ui-button-warning {
background: var(--color-status-warning-strong);
color: var(--color-status-warning-strong-foreground);
}
.ui-button-solid.ui-button-warning:hover {
background: var(--color-status-warning-strong-hover);
}
.ui-button-solid.ui-button-warning:active {
background: var(--color-status-warning-strong-active);
}
.ui-button-solid.ui-button-success {
background: var(--color-status-success-strong);
color: var(--color-status-success-strong-foreground);
}
.ui-button-solid.ui-button-success:hover {
background: var(--color-status-success-strong-hover);
}
.ui-button-solid.ui-button-success:active {
background: var(--color-status-success-strong-active);
}
.ui-button-solid.ui-button-danger {
background: var(--color-status-danger-strong);
color: var(--color-status-danger-strong-foreground);
}
.ui-button-solid.ui-button-danger:hover {
background: var(--color-status-danger-strong-hover);
}
.ui-button-solid.ui-button-danger:active {
background: var(--color-status-danger-strong-active);
}
.ui-button-soft.ui-button-default {
background: var(--color-surface-brand-soft);
color: var(--color-brand-primary-hover);
}
.ui-button-soft.ui-button-default:hover {
background: var(--color-surface-brand-soft-hover);
}
.ui-button-soft.ui-button-default:active {
background: var(--color-surface-brand-soft-active);
}
.ui-button-soft.ui-button-warning {
background: var(--color-status-warning-soft-bg);
color: var(--color-status-warning-soft-foreground-strong);
}
.ui-button-soft.ui-button-warning:hover {
background: var(--color-status-warning-soft-bg-hover);
}
.ui-button-soft.ui-button-warning:active {
background: var(--color-status-warning-soft-bg-active);
}
.ui-button-soft.ui-button-success {
background: var(--color-status-success-soft-bg);
color: var(--color-status-success-soft-foreground-strong);
}
.ui-button-soft.ui-button-success:hover {
background: var(--color-status-success-soft-bg-hover);
}
.ui-button-soft.ui-button-success:active {
background: var(--color-status-success-soft-bg-active);
}
.ui-button-soft.ui-button-danger {
background: var(--color-status-danger-soft-bg-alt);
color: var(--color-status-danger-soft-foreground-strong);
}
.ui-button-soft.ui-button-danger:hover {
background: var(--color-status-danger-soft-bg-hover);
}
.ui-button-soft.ui-button-danger:active {
background: var(--color-status-danger-soft-bg-active);
}

View file

@ -0,0 +1,27 @@
.ui-card {
background: var(--color-surface-default);
border: 1px solid var(--color-border-default);
border-radius: 1rem;
box-shadow: var(--shadow-surface-md);
}
.ui-card-header,
.ui-card-body,
.ui-card-footer {
padding: 1.25rem 1.5rem;
}
.ui-card-header,
.ui-card-footer {
border-color: var(--color-border-default);
}
.ui-card-header {
border-bottom-style: solid;
border-bottom-width: 1px;
}
.ui-card-footer {
border-top-style: solid;
border-top-width: 1px;
}

View file

@ -0,0 +1,163 @@
.catalog-page {
margin: 0 auto;
max-width: 72rem;
padding: 3rem 1.5rem 4rem;
}
.catalog-nav {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 0.75rem 1rem;
margin-bottom: 1.5rem;
}
.catalog-home-link,
.catalog-nav-link {
border-radius: 999px;
color: var(--color-text-muted);
display: inline-flex;
font-size: 0.9rem;
font-weight: 600;
padding: 0.55rem 0.9rem;
transition:
background-color 0.2s ease,
color 0.2s ease;
}
.catalog-home-link:hover,
.catalog-nav-link:hover {
background: var(--color-surface-muted);
color: var(--color-text-primary);
}
.catalog-nav-links {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.catalog-nav-link.is-active {
background: var(--color-surface-brand-soft);
color: var(--color-brand-primary-hover);
}
.catalog-page-header {
margin-bottom: 2rem;
}
.catalog-page-header h1 {
color: var(--color-text-primary);
font-size: 2.25rem;
line-height: 1.1;
margin: 0 0 0.75rem;
}
.catalog-page-header p {
color: var(--color-text-muted);
margin: 0;
max-width: 42rem;
}
.catalog-eyebrow {
color: var(--color-text-brand-strong) !important;
font-size: 0.8rem;
font-weight: 700;
letter-spacing: 0.08em;
margin-bottom: 0.75rem !important;
text-transform: uppercase;
}
.catalog-example-list,
.catalog-page-list {
display: grid;
gap: 1.25rem;
}
.catalog-example,
.catalog-page-link-card {
background: var(--color-surface-default);
border: 1px solid var(--color-border-default);
border-radius: 1rem;
box-shadow: var(--shadow-surface-sm);
padding: 1.5rem;
}
.catalog-page-link-card {
display: block;
}
.catalog-example-copy h2,
.catalog-page-link-card h2 {
color: var(--color-text-primary);
font-size: 1.125rem;
margin: 0 0 0.5rem;
}
.catalog-example-copy p,
.catalog-page-link-card p {
color: var(--color-text-muted);
margin: 0;
}
.catalog-example-preview {
align-items: flex-start;
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin-top: 1rem;
}
.catalog-inline {
display: inline-flex;
}
.catalog-spacing-row {
align-items: center;
display: flex;
gap: 0;
}
.catalog-spacing-column {
display: flex;
flex-direction: column;
gap: 0;
width: 100%;
}
.catalog-example-snippet {
background: var(--color-surface-muted-inverse);
border-radius: 0.875rem;
color: var(--color-surface-neutral-hover);
margin: 1rem 0 0;
overflow-x: auto;
padding: 1rem;
}
.catalog-example-snippet code {
font-family:
ui-monospace,
SFMono-Regular,
"SF Mono",
Menlo,
Monaco,
Consolas,
"Liberation Mono",
monospace;
font-size: 0.875rem;
}
.catalog-page-link {
color: var(--color-text-brand-strong) !important;
font-family:
ui-monospace,
SFMono-Regular,
"SF Mono",
Menlo,
Monaco,
Consolas,
"Liberation Mono",
monospace;
font-size: 0.875rem;
margin-top: 1rem !important;
}

View file

@ -0,0 +1,40 @@
.ui-empty-state {
align-items: center;
border: 1px dashed var(--color-border-subtle);
border-radius: 1rem;
color: var(--color-text-muted);
display: flex;
flex-direction: column;
gap: 0.75rem;
justify-content: center;
padding: 3rem 1.5rem;
text-align: center;
}
.ui-empty-state-title {
color: var(--color-text-primary);
font-size: 1.125rem;
font-weight: 700;
margin: 0;
}
.ui-empty-state-icon {
align-items: center;
background: var(--color-surface-muted);
border-radius: 999px;
color: var(--color-text-faint);
display: inline-flex;
height: 4rem;
justify-content: center;
width: 4rem;
}
.ui-empty-state-icon svg {
height: 2rem;
width: 2rem;
}
.ui-empty-state-description {
margin: 0;
max-width: 32rem;
}

View file

@ -0,0 +1,22 @@
.ui-form-field {
display: grid;
gap: 0.5rem;
}
.ui-form-label {
color: var(--color-text-primary);
font-size: 0.95rem;
font-weight: 600;
}
.ui-form-hint {
color: var(--color-text-muted);
font-size: 0.875rem;
margin: 0;
}
.ui-form-error {
color: var(--color-status-danger-foreground);
font-size: 0.875rem;
margin: 0;
}

View file

@ -0,0 +1,50 @@
.ui-icon-button {
align-items: center;
appearance: none;
background: transparent;
border: 0;
border-radius: 0.5rem;
cursor: pointer;
display: inline-flex;
justify-content: center;
min-height: 44px;
min-width: 44px;
padding: 0.5rem;
transition:
background-color 0.2s ease,
color 0.2s ease;
}
.ui-icon-button:focus-visible,
.borderless-icon-button:focus-visible {
box-shadow: 0 0 0 3px var(--color-focus-ring);
outline: none;
}
.ui-icon-button-solid.ui-icon-button-neutral {
color: var(--color-text-muted);
}
.ui-icon-button-solid.ui-icon-button-neutral:hover {
background: var(--color-surface-neutral-hover);
color: var(--color-text-primary);
}
.borderless-icon-button {
appearance: none;
background: transparent;
border: 0;
box-shadow: none;
cursor: pointer;
outline: none;
}
.ui-icon-button-ghost.ui-icon-button-neutral,
.ui-icon-button-ghost.ui-icon-button-danger {
color: var(--color-text-faint);
}
.borderless-icon-button svg {
height: 1rem;
width: 1rem;
}

View file

@ -0,0 +1,22 @@
.ui-input {
appearance: none;
background: var(--color-surface-default);
border: 1px solid var(--color-border-default);
border-radius: 0.75rem;
color: var(--color-text-primary);
font: inherit;
line-height: 1.4;
min-height: 44px;
padding: 0.75rem 0.95rem;
width: 100%;
}
.ui-input::placeholder {
color: var(--color-text-faint);
}
.ui-input:focus {
border-color: var(--color-brand-focus);
box-shadow: 0 0 0 3px var(--color-focus-ring-strong);
outline: none;
}

View file

@ -0,0 +1,53 @@
.ui-modal-backdrop {
align-items: center;
background: var(--overlay-backdrop-default);
display: flex;
inset: 0;
justify-content: center;
padding: 1rem;
position: fixed;
z-index: 40;
}
.ui-modal-panel {
background: var(--color-surface-default);
border: 1px solid var(--color-border-default);
border-radius: 1rem;
box-shadow: var(--shadow-surface-lg);
max-width: 32rem;
width: min(100%, 32rem);
}
.ui-modal-header,
.ui-modal-body,
.ui-modal-actions {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.ui-modal-header {
border-bottom: 1px solid var(--color-border-default);
padding-bottom: 1rem;
padding-top: 1.25rem;
}
.ui-modal-header h2 {
color: var(--color-text-primary);
font-size: 1.125rem;
font-weight: 700;
margin: 0;
}
.ui-modal-body {
padding-bottom: 1.25rem;
padding-top: 1.25rem;
}
.ui-modal-actions {
border-top: 1px solid var(--color-border-default);
display: flex;
gap: 0.75rem;
justify-content: flex-end;
padding-bottom: 1rem;
padding-top: 1rem;
}

View file

@ -0,0 +1,48 @@
.ui-space-x {
display: inline-block;
flex-shrink: 0;
}
.ui-space-y {
display: block;
}
.ui-space-x-xs {
width: 0.25rem;
}
.ui-space-x-sm {
width: 0.5rem;
}
.ui-space-x-md {
width: 0.75rem;
}
.ui-space-x-lg {
width: 1rem;
}
.ui-space-x-xl {
width: 1.5rem;
}
.ui-space-y-xs {
height: 0.25rem;
}
.ui-space-y-sm {
height: 0.5rem;
}
.ui-space-y-md {
height: 0.75rem;
}
.ui-space-y-lg {
height: 1rem;
}
.ui-space-y-xl {
height: 1.5rem;
}

View file

@ -0,0 +1,10 @@
.ui-table-shell {
overflow-x: auto;
width: 100%;
}
.ui-table {
border-collapse: collapse;
min-width: 100%;
width: 100%;
}

View file

@ -0,0 +1,23 @@
.ui-textarea {
appearance: none;
background: var(--color-surface-default);
border: 1px solid var(--color-border-default);
border-radius: 0.75rem;
color: var(--color-text-primary);
font: inherit;
line-height: 1.4;
min-height: 7rem;
padding: 0.85rem 0.95rem;
resize: vertical;
width: 100%;
}
.ui-textarea::placeholder {
color: var(--color-text-faint);
}
.ui-textarea:focus {
border-color: var(--color-brand-focus);
box-shadow: 0 0 0 3px var(--color-focus-ring-strong);
outline: none;
}

View file

@ -191,6 +191,11 @@ func TestSharedSemanticClassesExistInStylesheet(t *testing.T) {
css := string(body)
for _, want := range []string{
`Code generated by cmd/buildstyles`,
`--color-text-primary`,
`--color-surface-default`,
`--color-status-warning-soft-bg`,
`--shadow-surface-md`,
`--overlay-backdrop-default`,
`.ui-button-solid.ui-button-default`,
`.ui-badge-warning`,
`.ui-card`,

View file

@ -183,7 +183,7 @@ templ TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) {
Label: tablo.StatusLabel,
Variant: badgeVariantForTone(tablo.StatusTone),
})
<div class="flex items-center gap-2">
<div class="flex items-center gap-3">
@EditTabloButton(tablo.EditRequestURL)
@BorderlessDeleteButton(tablo.DeleteRequestURL)
</div>

View file

@ -588,7 +588,7 @@ func TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) templ.C
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<div class=\"flex items-center gap-2\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<div class=\"flex items-center gap-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

File diff suppressed because it is too large Load diff

View file

@ -49,6 +49,9 @@
--radius-xl: 0.75rem;
--default-transition-duration: 150ms;
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
--color-surface-muted: #f9fafb;
--color-text-muted: #6b7280;
--color-border-subtle: #eaecf0;
}
.pointer-events-none {
pointer-events: none;