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:
parent
0ac8bd0fc9
commit
8bcf81a3f1
24 changed files with 4733 additions and 800 deletions
|
|
@ -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"
|
||||
```
|
||||
|
|
@ -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"
|
||||
```
|
||||
|
|
@ -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
|
||||
65
go-backend/cmd/buildstyles/main.go
Normal file
65
go-backend/cmd/buildstyles/main.go
Normal 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
|
||||
}
|
||||
63
go-backend/cmd/buildstyles/main_test.go
Normal file
63
go-backend/cmd/buildstyles/main_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
1891
go-backend/internal/web/ui/app.css
Normal file
1891
go-backend/internal/web/ui/app.css
Normal file
File diff suppressed because it is too large
Load diff
33
go-backend/internal/web/ui/badge.css
Normal file
33
go-backend/internal/web/ui/badge.css
Normal 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);
|
||||
}
|
||||
223
go-backend/internal/web/ui/base.css
Normal file
223
go-backend/internal/web/ui/base.css
Normal 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;
|
||||
}
|
||||
162
go-backend/internal/web/ui/button.css
Normal file
162
go-backend/internal/web/ui/button.css
Normal 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);
|
||||
}
|
||||
27
go-backend/internal/web/ui/card.css
Normal file
27
go-backend/internal/web/ui/card.css
Normal 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;
|
||||
}
|
||||
163
go-backend/internal/web/ui/catalog/catalog.css
Normal file
163
go-backend/internal/web/ui/catalog/catalog.css
Normal 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;
|
||||
}
|
||||
40
go-backend/internal/web/ui/empty-state.css
Normal file
40
go-backend/internal/web/ui/empty-state.css
Normal 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;
|
||||
}
|
||||
22
go-backend/internal/web/ui/form-field.css
Normal file
22
go-backend/internal/web/ui/form-field.css
Normal 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;
|
||||
}
|
||||
50
go-backend/internal/web/ui/icon-button.css
Normal file
50
go-backend/internal/web/ui/icon-button.css
Normal 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;
|
||||
}
|
||||
22
go-backend/internal/web/ui/input.css
Normal file
22
go-backend/internal/web/ui/input.css
Normal 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;
|
||||
}
|
||||
53
go-backend/internal/web/ui/modal.css
Normal file
53
go-backend/internal/web/ui/modal.css
Normal 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;
|
||||
}
|
||||
48
go-backend/internal/web/ui/spacing.css
Normal file
48
go-backend/internal/web/ui/spacing.css
Normal 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;
|
||||
}
|
||||
10
go-backend/internal/web/ui/table.css
Normal file
10
go-backend/internal/web/ui/table.css
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
.ui-table-shell {
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ui-table {
|
||||
border-collapse: collapse;
|
||||
min-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
23
go-backend/internal/web/ui/textarea.css
Normal file
23
go-backend/internal/web/ui/textarea.css
Normal 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;
|
||||
}
|
||||
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue