diff --git a/docs/superpowers/plans/2026-05-10-go-backend-css-sources-per-primitive.md b/docs/superpowers/plans/2026-05-10-go-backend-css-sources-per-primitive.md new file mode 100644 index 0000000..e35e23c --- /dev/null +++ b/docs/superpowers/plans/2026-05-10-go-backend-css-sources-per-primitive.md @@ -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" +``` diff --git a/docs/superpowers/plans/2026-05-10-go-backend-semantic-css-color-tokens.md b/docs/superpowers/plans/2026-05-10-go-backend-semantic-css-color-tokens.md new file mode 100644 index 0000000..d3dedf3 --- /dev/null +++ b/docs/superpowers/plans/2026-05-10-go-backend-semantic-css-color-tokens.md @@ -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" +``` diff --git a/docs/superpowers/specs/2026-05-10-go-backend-semantic-css-color-tokens-design.md b/docs/superpowers/specs/2026-05-10-go-backend-semantic-css-color-tokens-design.md new file mode 100644 index 0000000..0dc50a5 --- /dev/null +++ b/docs/superpowers/specs/2026-05-10-go-backend-semantic-css-color-tokens-design.md @@ -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 diff --git a/go-backend/cmd/buildstyles/main.go b/go-backend/cmd/buildstyles/main.go new file mode 100644 index 0000000..4cdd36c --- /dev/null +++ b/go-backend/cmd/buildstyles/main.go @@ -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 +} diff --git a/go-backend/cmd/buildstyles/main_test.go b/go-backend/cmd/buildstyles/main_test.go new file mode 100644 index 0000000..2db3b17 --- /dev/null +++ b/go-backend/cmd/buildstyles/main_test.go @@ -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) + } +} diff --git a/go-backend/internal/web/ui/app.css b/go-backend/internal/web/ui/app.css new file mode 100644 index 0000000..026863b --- /dev/null +++ b/go-backend/internal/web/ui/app.css @@ -0,0 +1,1891 @@ +.app-shell, +.login-screen { + min-height: 100vh; +} + +.app-shell { + background: var(--background); +} + +.login-screen { + align-items: center; + background: var(--gradient-shell); + display: flex; + justify-content: center; + overflow: hidden; + padding: 2rem 1rem; + position: relative; +} + +.background-layer { + inset: 0; + overflow: hidden; + pointer-events: none; + position: absolute; +} + +.background-logo { + position: absolute; +} + +.logo-asset { + display: block; + height: auto; + object-fit: contain; + width: 100%; +} + +.size-06 { width: 1.5rem; height: 1.5rem; } +.size-07 { width: 1.75rem; height: 1.75rem; } +.size-08 { width: 2rem; height: 2rem; } +.size-09 { width: 2.25rem; height: 2.25rem; } +.size-10 { width: 2.5rem; height: 2.5rem; } +.size-11 { width: 2.75rem; height: 2.75rem; } +.size-12 { width: 3rem; height: 3rem; } +.size-13 { width: 3.25rem; height: 3.25rem; } +.size-14 { width: 3.5rem; height: 3.5rem; } +.size-15 { width: 3.75rem; height: 3.75rem; } +.size-16 { width: 4rem; height: 4rem; } +.size-18 { width: 4.5rem; height: 4.5rem; } +.size-20 { width: 5rem; height: 5rem; } + +.opacity-02 { opacity: 0.2; } +.opacity-03 { opacity: 0.3; } +.opacity-04 { opacity: 0.4; } +.opacity-05 { opacity: 0.5; } + +.bg-01 { top: 25%; left: 0; } +.bg-02 { top: 33%; left: 0; } +.bg-03 { top: 50%; left: 0; } +.bg-04 { top: 66%; left: 0; } +.bg-05 { top: 75%; left: 0; } +.bg-06 { top: 0; left: 25%; } +.bg-07 { top: 0; left: 50%; } +.bg-08 { top: 0; left: 75%; } +.bg-09 { top: 0; left: 16.66%; } +.bg-10 { top: 0; left: 83.33%; } +.bg-11, +.bg-12, +.bg-13, +.bg-14, +.bg-15 { + left: 50%; + top: 50%; +} +.bg-16 { top: 25%; left: 0; } +.bg-17 { top: 50%; left: 0; } +.bg-18 { top: 75%; left: 0; } +.bg-19 { top: 0; left: 25%; } +.bg-20 { top: 0; left: 75%; } +.bg-21 { top: 16.66%; left: 33.33%; } +.bg-22 { top: 33.33%; left: 66.66%; } +.bg-23 { top: 66.66%; left: 25%; } +.bg-24 { top: 83.33%; left: 75%; } +.bg-25 { top: 12.5%; left: 0; } +.bg-26 { top: 37.5%; left: 0; } +.bg-27 { top: 62.5%; left: 0; } +.bg-28 { top: 87.5%; left: 0; } +.bg-29 { top: 0; left: 0; } +.bg-30 { top: 0; right: 0; } +.bg-31 { bottom: 0; left: 0; } +.bg-32 { bottom: 0; right: 0; } +.bg-33 { top: 20%; left: 20%; } +.bg-34 { top: 40%; left: 80%; } +.bg-35 { top: 80%; left: 40%; } + +.card-wrap { + max-width: 32rem; + position: relative; + transition: transform 0.2s ease-out; + width: 100%; + z-index: 1; +} + +.card-glow { + background: var(--gradient-card-glow); + border-radius: 1rem; + filter: blur(24px); + inset: 0; + position: absolute; + z-index: -1; +} + +.auth-card-shell { + backdrop-filter: blur(12px); + background: var(--card); + border: 1px solid var(--border); + border-radius: 1rem; + box-shadow: var(--shadow-auth-card); + padding: 1.25rem; + position: relative; +} + +.auth-card-topbar { + align-items: center; + display: flex; + justify-content: space-between; + margin-bottom: 1.5rem; +} + +.back-home-link { + align-items: center; + color: var(--muted-foreground); + display: inline-flex; + font-size: 0.875rem; + transition: color 0.2s ease; +} + +.back-home-link:hover, +.theme-toggle-button:hover, +.signup-link:hover { + color: var(--foreground); +} + +.back-home-icon { + height: 1rem; + margin-right: 0.5rem; + width: 1rem; +} + +.theme-toggle-button { + align-items: center; + background: transparent; + border: 0; + border-radius: 0.5rem; + color: var(--muted-foreground); + cursor: pointer; + display: inline-flex; + height: 2.25rem; + justify-content: center; + padding: 0.5rem; + transition: + background-color 0.2s ease, + color 0.2s ease; + width: 2.25rem; +} + +.theme-toggle-button:hover { + background: var(--accent); +} + +.theme-toggle-icon { + height: 1.25rem; + width: 1.25rem; +} + +.brand-header { + display: flex; + justify-content: center; + margin-bottom: 1.5rem; +} + +.brand-logo { + display: block; + height: 4rem; + object-fit: contain; + width: 4rem; +} + +.title-group { + margin-bottom: 1.5rem; + text-align: center; +} + +.title-group h1 { + font-size: clamp(1.5rem, 4vw, 1.875rem); + font-weight: 700; + margin: 0; +} + +.new-experience-link-wrap { + margin-bottom: 1.5rem; + text-align: center; +} + +.new-experience-link { + color: var(--color-text-brand); + display: inline-flex; + font-size: 0.875rem; + font-weight: 500; + transition: color 0.2s ease; +} + +.new-experience-link:hover, +.forgot-password-link:hover { + color: var(--color-text-brand-hover); +} + +.auth-body { + align-items: center; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.login-form { + display: flex; + flex-direction: column; + gap: 1rem; + margin: 0 auto; + max-width: 28rem; + width: 100%; +} + +.field-stack { + display: grid; + gap: 0.5rem; +} + +.field-stack label { + font-size: 0.875rem; + font-weight: 500; +} + +.field-stack input { + background: var(--background); + border: 1px solid var(--input); + border-radius: 0.375rem; + color: var(--foreground); + height: 2.5rem; + padding: 0.5rem 0.75rem; + width: 100%; +} + +.field-stack input:focus { + box-shadow: 0 0 0 1px var(--ring); + outline: none; +} + +.field-stack input::placeholder { + color: var(--muted-foreground); +} + +.forgot-password-row { + display: flex; + justify-content: flex-end; +} + +.forgot-password-link { + color: var(--color-text-brand-strong); + font-size: 0.875rem; + transition: color 0.2s ease; +} + +.submit-button { + align-items: center; + background: var(--primary); + border: 0; + border-radius: 0.375rem; + color: var(--primary-foreground); + cursor: pointer; + display: inline-flex; + font-size: 0.875rem; + font-weight: 500; + height: 2.25rem; + justify-content: center; + padding: 0.5rem 1rem; + transition: opacity 0.2s ease; + width: 100%; +} + +.submit-button:hover { + opacity: 0.9; +} + +.divider-row { + align-items: center; + display: flex; + gap: 0.25rem; + margin: 0.5rem 0 0; + position: relative; + width: 100%; +} + +.divider-line { + border-top: 1px solid var(--border); + flex: 1; +} + +.divider-pill { + background: var(--background); + border-radius: 999px; + color: var(--muted-foreground); + font-size: 0.875rem; + font-weight: 500; + padding: 0.25rem 1rem; + position: relative; + z-index: 1; +} + +.signup-copy { + color: var(--muted-foreground); + font-size: 0.875rem; + margin: 0; + text-align: center; +} + +.signup-link { + border-radius: 0.375rem; + color: var(--foreground); + display: inline-block; + font-weight: 500; + margin-left: 0.2rem; + padding: 0.25rem 0.5rem; + transition: + color 0.2s ease, + background-color 0.2s ease; +} + +.signup-link:hover { + background: var(--accent); +} + +.status-slot { + min-height: 0.25rem; +} + +.status-banner { + border: 1px solid; + border-radius: 0.5rem; + font-size: 0.875rem; + padding: 0.75rem 0.875rem; +} + +.status-success { + background: var(--color-status-success-banner-bg); + border-color: var(--color-status-success-banner-border); + color: var(--color-status-success-banner-foreground); +} + +.status-error { + background: var(--color-status-danger-banner-bg); + border-color: var(--color-status-danger-banner-border); + color: var(--color-status-danger-banner-foreground); +} + +.gsi-material-button { + -moz-user-select: none; + -ms-user-select: none; + -webkit-appearance: none; + -webkit-user-select: none; + background-color: var(--color-surface-default); + background-image: none; + border: 1px solid var(--color-border-google); + border-radius: 20px; + box-sizing: border-box; + color: var(--color-text-google); + cursor: pointer; + font-family: "Roboto", Arial, sans-serif; + font-size: 14px; + height: 40px; + letter-spacing: 0.25px; + max-width: 400px; + min-width: min-content; + outline: none; + overflow: hidden; + padding: 0 12px; + position: relative; + text-align: center; + transition: + background-color 0.218s, + border-color 0.218s, + box-shadow 0.218s; + vertical-align: middle; + white-space: nowrap; + width: 100%; +} + +.gsi-material-button .gsi-material-button-icon { + height: 20px; + margin-right: 12px; + min-width: 20px; + width: 20px; +} + +.gsi-material-button .gsi-material-button-content-wrapper { + align-items: center; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + height: 100%; + justify-content: space-between; + position: relative; + width: 100%; +} + +.gsi-material-button .gsi-material-button-contents { + flex-grow: 1; + font-family: "Roboto", Arial, sans-serif; + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: top; +} + +.gsi-material-button .gsi-material-button-state { + bottom: 0; + left: 0; + opacity: 0; + position: absolute; + right: 0; + top: 0; + transition: opacity 0.218s; +} + +.gsi-material-button:not(:disabled):active .gsi-material-button-state, +.gsi-material-button:not(:disabled):focus .gsi-material-button-state { + background-color: var(--overlay-google-state); + opacity: 0.12; +} + +.gsi-material-button:not(:disabled):hover { + box-shadow: var(--shadow-google-button); +} + +.gsi-material-button:not(:disabled):hover .gsi-material-button-state { + background-color: var(--overlay-google-state); + opacity: 0.08; +} + +.home-shell { + background: var(--gradient-shell); + min-height: 100vh; +} + +.dashboard-shell { + background: var(--gradient-shell); + color: var(--foreground); + display: grid; + grid-template-columns: minmax(16rem, 18rem) 1fr; + min-height: 100vh; +} + +.dashboard-sidebar { + padding-left: env(safe-area-inset-left, 0px); +} + +.sidebar-nav-shell { + background: var(--color-surface-elevated); + border-right: 1px solid var(--color-border-panel); + box-shadow: var(--shadow-sidebar); + display: flex; + flex-direction: column; + height: 100vh; + overflow-x: hidden; + overflow-y: auto; + padding: env(safe-area-inset-top, 0px) 0.75rem env(safe-area-inset-bottom, 0px) 0; + position: sticky; + top: 0; +} + +.sidebar-brand { + align-items: center; + display: flex; + justify-content: center; + padding: 0.75rem 0.5rem; + position: relative; +} + +.sidebar-brand-link { + align-items: center; + display: flex; + flex-direction: column; + gap: 0.5rem; + width: 100%; +} + +.sidebar-brand-logo { + border-radius: 0.75rem; + height: 4rem; + object-fit: cover; + width: 4rem; +} + +.sidebar-brand-title { + color: var(--color-text-heading-alt); + font-size: 1.125rem; + font-weight: 700; + margin: 0; +} + +.sidebar-collapse-button { + align-items: center; + background: var(--color-surface-elevated-strong); + border: 0; + border-radius: 999px; + box-shadow: var(--shadow-floating-control); + color: var(--color-text-muted); + cursor: pointer; + display: inline-flex; + height: 1.5rem; + justify-content: center; + padding: 0.25rem; + position: absolute; + right: 0.75rem; + top: 0.5rem; + width: 1.5rem; +} + +.sidebar-collapse-button svg { + height: 1rem; + width: 1rem; +} + +.sidebar-primary { + display: flex; + flex: 1; + flex-direction: column; +} + +.sidebar-list { + display: grid; + gap: 0; + list-style: none; + margin: 0; + padding: 0.75rem 0 0; +} + +.sidebar-divider { + margin: 0.5rem 0; + padding: 0 0.875rem; +} + +.sidebar-divider hr, +.sidebar-projects hr { + border: 0; + border-top: 1px solid var(--color-border-panel-muted); + margin: 0; +} + +.sidebar-nav-item { + border-radius: 0.9rem; + color: var(--color-text-muted); + font-weight: 500; + margin: 0 0.5rem; + padding: 0.15rem 0; + transition: background-color 0.2s ease, color 0.2s ease; +} + +.sidebar-nav-item:hover { + background: var(--overlay-dark-soft); + color: var(--color-surface-muted-inverse); +} + +.sidebar-nav-item.is-active { + background: var(--overlay-brand-soft-strong); + color: var(--color-text-brand); + font-weight: 600; +} + +.sidebar-nav-link { + display: block; + width: 100%; +} + +.sidebar-nav-link-inner { + align-items: center; + display: flex; + gap: 0.75rem; + padding: 0.6rem 0.95rem; +} + +.sidebar-nav-icon { + align-items: center; + display: inline-flex; + justify-content: center; +} + +.sidebar-nav-icon svg { + height: 1.35rem; + width: 1.35rem; +} + +.sidebar-nav-label { + font-size: 1rem; +} + +.sidebar-projects { + margin-top: 0.4rem; + padding: 0 0.75rem 0.75rem; +} + +.sidebar-section-label { + color: var(--color-text-muted); + font-size: 0.625rem; + font-weight: 700; + letter-spacing: 0.14em; + margin: 0.9rem 0 0.65rem; + padding: 0 0.5rem; + text-transform: uppercase; +} + +.sidebar-project-list { + display: grid; + gap: 0.1rem; + list-style: none; + margin: 0; + padding: 0; +} + +.sidebar-project-link { + align-items: center; + border-radius: 0.85rem; + color: var(--color-text-muted); + display: flex; + gap: 0.65rem; + padding: 0.48rem 0.5rem; + transition: background-color 0.2s ease, color 0.2s ease; +} + +.sidebar-project-link:hover { + background: var(--overlay-dark-soft); + color: var(--color-surface-muted-inverse); +} + +.sidebar-project-icon { + align-items: center; + border: 1px solid var(--color-border-panel-strong); + border-radius: 999px; + display: inline-flex; + flex-shrink: 0; + height: 1.55rem; + justify-content: center; + width: 1.55rem; +} + +.sidebar-project-icon svg { + height: 0.9rem; + width: 0.9rem; +} + +.sidebar-project-label { + flex: 1; + font-size: 0.9rem; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sidebar-footer-links { + margin-top: auto; + padding-bottom: 0.25rem; +} + +.sidebar-organization { + background: var(--color-surface-elevated); + padding: 0 0.5rem 0.9rem; +} + +.organization-button { + align-items: center; + background: transparent; + border: 0; + border-radius: 0.95rem; + cursor: pointer; + display: flex; + gap: 0.65rem; + padding: 0.55rem 0.65rem; + text-align: left; + transition: background-color 0.2s ease; + width: 100%; +} + +.organization-button:hover { + background: var(--overlay-dark-soft); +} + +.organization-avatar { + border-radius: 999px; + display: inline-flex; + flex-shrink: 0; + height: 1.75rem; + overflow: hidden; + width: 1.75rem; +} + +.organization-avatar img { + aspect-ratio: 1; + height: 100%; + object-fit: cover; + width: 100%; +} + +.organization-copy { + display: flex; + flex-direction: column; + min-width: 0; +} + +.organization-name { + color: var(--color-text-body-subtle); + font-size: 0.95rem; + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.organization-meta { + color: var(--color-text-muted); + font-size: 0.75rem; +} + +.dashboard-main { + display: flex; + flex-direction: column; + gap: 1.5rem; + min-width: 0; + padding: 2rem; +} + +.overview-page { + display: flex; + flex-direction: column; + gap: 1.5rem; + min-height: 100%; +} + +.overview-header { + padding: 0 0 0.25rem; +} + +.overview-date { + color: var(--color-text-secondary); + font-size: 1rem; + font-weight: 500; + margin: 0 0 0.5rem; +} + +.overview-header-row { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 1rem; + justify-content: space-between; +} + +.overview-greeting { + color: var(--color-text-secondary); + font-size: clamp(1.4rem, 2vw, 1.75rem); + font-weight: 500; + margin: 0; +} + +.overview-greeting span { + color: var(--color-surface-muted-inverse); +} + +.overview-header-actions { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +.overview-badge { + align-items: center; + background: var(--gradient-overview-badge); + border: 0; + border-radius: 999px; + color: var(--color-text-inverse); + display: inline-flex; + font-size: 0.75rem; + font-weight: 600; + justify-content: center; + min-height: 1.85rem; + padding: 0.25rem 0.75rem; +} + +.overview-logout-form { + margin: 0; +} + +.overview-logout-button { + background: var(--color-surface-elevated); + border: 1px solid var(--color-border-subtle); + border-radius: 0.85rem; + color: var(--color-text-secondary); + cursor: pointer; + font-weight: 600; + min-height: 2.75rem; + padding: 0.65rem 1rem; +} + +.overview-actions { + display: grid; + gap: 1rem; + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.quick-action-card { + align-items: center; + background: var(--color-surface-default); + border: 1px solid var(--color-border-subtle); + border-radius: 1rem; + cursor: pointer; + display: flex; + gap: 0.9rem; + min-height: 5rem; + padding: 0.9rem; + text-align: left; + transition: box-shadow 0.2s ease, transform 0.2s ease; +} + +.quick-action-card:hover, +.project-card:hover { + box-shadow: var(--shadow-surface-hover); +} + +.quick-action-icon { + align-items: center; + background: var(--color-surface-brand-muted); + border-radius: 0.7rem; + color: var(--color-text-brand-accent); + display: inline-flex; + flex-shrink: 0; + height: 2.5rem; + justify-content: center; + width: 2.5rem; +} + +.quick-action-icon svg { + height: 1.5rem; + width: 1.5rem; +} + +.quick-action-copy { + flex: 1; + min-width: 0; +} + +.quick-action-title { + color: var(--color-surface-muted-inverse); + font-size: 1.05rem; + font-weight: 600; + line-height: 1.2; +} + +.quick-action-copy p { + color: var(--color-text-muted); + font-size: 0.9rem; + margin: 0.25rem 0 0; +} + +.overview-section { + padding: 0.25rem 0 0; +} + +.overview-section-heading { + align-items: center; + display: flex; + justify-content: space-between; + margin-bottom: 1.5rem; +} + +.overview-section-heading h3, +.tasks-section-header h3 { + color: var(--color-surface-muted-inverse); + font-size: 1.6rem; + font-weight: 600; + margin: 0; +} + +.project-grid { + display: grid; + gap: 1.25rem; + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.project-card { + background: var(--color-surface-default); + border: 1px solid var(--color-border-subtle); + border-radius: 1rem; + cursor: pointer; + padding: 1rem; + transition: box-shadow 0.2s ease; +} + +.project-card-top { + align-items: center; + display: flex; + justify-content: space-between; + margin-bottom: 1rem; +} + +.project-status, +.task-status { + border: 1px solid transparent; + border-radius: 999px; + display: inline-flex; + font-size: 0.75rem; + font-weight: 600; + padding: 0.25rem 0.7rem; +} + +.tone-warning { + background: var(--color-status-warning-emphasis-bg); + border-color: var(--color-status-warning-emphasis-border); + color: var(--color-status-warning-emphasis-foreground); +} + +.tone-info { + background: var(--color-status-info-soft-bg); + border-color: var(--color-status-info-soft-border); + color: var(--color-status-info-foreground); +} + +.tone-success { + background: var(--color-status-success-soft-bg); + border-color: var(--color-status-success-soft-border); + color: var(--color-status-success-foreground); +} + + +.project-card-top .borderless-icon-button { + padding: 0; +} + +.project-card-top .borderless-icon-button.ui-icon-button-ghost.ui-icon-button-neutral:hover { + color: var(--color-surface-muted-inverse); +} + +.project-card-top .borderless-icon-button.ui-icon-button-ghost.ui-icon-button-danger:hover { + color: var(--color-status-danger-icon-hover); +} + +.borderless-icon-button svg, +.project-date-row svg, +.overview-more-button svg, +.tasks-add-button svg, +.task-check svg { + height: 1rem; + width: 1rem; +} + +td.text-right .borderless-icon-button { + align-items: center; + border-radius: 0.25rem; + color: var(--color-text-faint); + display: inline-flex; + justify-content: center; + min-height: 44px; + min-width: 44px; + padding: 0.5rem; + transition: color 0.2s; +} + +td.text-right .borderless-icon-button.ui-icon-button-ghost.ui-icon-button-neutral:hover { + color: var(--color-surface-muted-inverse); +} + +td.text-right .borderless-icon-button.ui-icon-button-ghost.ui-icon-button-danger:hover { + color: var(--color-status-danger-icon-hover); +} + +.project-card-title-row { + align-items: center; + display: flex; + gap: 0.75rem; + margin-bottom: 1rem; +} + +.project-avatar { + align-items: center; + background: var(--project-color, var(--color-project-fallback)); + border-radius: 0.85rem; + color: var(--color-text-inverse); + display: inline-flex; + flex-shrink: 0; + font-size: 1.1rem; + font-weight: 700; + height: 3rem; + justify-content: center; + width: 3rem; +} + +.project-list-icon { + background: var(--project-color, var(--color-project-fallback)); + color: var(--color-text-inverse); +} + +.project-accent-blue { + background: var(--color-project-fallback); +} + +.project-accent-purple { + background: var(--color-project-accent-purple); +} + +.project-accent-red { + background: var(--color-project-accent-red); +} + +.project-card-title-row h4 { + color: var(--color-surface-muted-inverse); + flex: 1; + font-size: 1rem; + font-weight: 600; + margin: 0; + overflow: hidden; + text-overflow: ellipsis; +} + +.project-date-row { + align-items: center; + color: var(--color-text-muted); + display: flex; + font-size: 0.875rem; + gap: 0.5rem; + margin-bottom: 1rem; +} + +.project-progress-label { + align-items: center; + color: var(--color-text-muted); + display: flex; + font-size: 0.875rem; + justify-content: space-between; + margin-bottom: 0.5rem; +} + +.project-progress-label strong { + color: var(--color-surface-muted-inverse); +} + +.project-progress-track { + background: var(--color-surface-muted); + border-radius: 999px; + height: 0.5rem; + overflow: hidden; +} + +.project-progress-bar { + background: var(--project-color, var(--color-project-fallback)); + border-radius: 999px; + height: 100%; +} + +.tablo-color-picker { + max-width: 5rem; + min-height: 44px; + padding: 0.4rem; +} + +.overview-more-row { + display: flex; + justify-content: center; + margin-top: 1.5rem; +} + +.overview-more-button { + align-items: center; + background: transparent; + border: 0; + color: var(--color-text-brand-strong); + cursor: pointer; + display: inline-flex; + font-size: 0.875rem; + font-weight: 600; + gap: 0.4rem; +} + +.app-section-page { + min-height: 100vh; + padding: 2rem; +} + +.app-section-surface { + background: var(--gradient-app-surface); + border: 1px solid var(--color-border-subtle); + border-radius: 1.5rem; + box-shadow: var(--shadow-surface-lg); + max-width: 52rem; + padding: 2rem; +} + +.app-section-eyebrow { + color: var(--color-text-brand-strong); + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.08em; + margin-bottom: 0.9rem; + text-transform: uppercase; +} + +.app-section-surface h2 { + color: var(--color-surface-muted-inverse); + font-size: clamp(1.8rem, 4vw, 2.6rem); + line-height: 1.05; + margin: 0; +} + +.app-section-surface p { + color: var(--color-text-secondary); + font-size: 1rem; + line-height: 1.8; + margin: 1rem 0 0; + max-width: 40rem; +} + +.not-found-page { + align-items: center; + background: var(--gradient-not-found-bg); + display: flex; + justify-content: center; + min-height: 100vh; + padding: 3rem 1.5rem; +} + +.not-found-surface { + backdrop-filter: blur(18px); + background: var(--color-surface-overlay); + border: 1px solid var(--color-border-overlay); + border-radius: 2rem; + box-shadow: var(--shadow-surface-xl); + padding: 3rem; + width: min(100%, 44rem); +} + +.not-found-eyebrow { + align-items: center; + background: var(--overlay-brand-soft); + border-radius: 999px; + color: var(--color-text-brand-strong); + display: inline-flex; + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.08em; + padding: 0.45rem 0.85rem; + text-transform: uppercase; +} + +.not-found-code { + color: var(--color-surface-muted-inverse); + font-size: clamp(4.75rem, 11vw, 7.5rem); + font-weight: 800; + letter-spacing: -0.08em; + line-height: 0.95; + margin-top: 1.4rem; +} + +.not-found-surface h2 { + color: var(--color-surface-muted-inverse); + font-size: clamp(1.9rem, 4vw, 2.8rem); + line-height: 1.05; + margin: 1rem 0 0; +} + +.not-found-surface p { + color: var(--color-text-secondary); + font-size: 1rem; + line-height: 1.75; + margin: 1rem 0 0; + max-width: 36rem; +} + +.not-found-actions { + display: flex; + flex-wrap: wrap; + gap: 0.9rem; + margin-top: 2rem; +} + +.not-found-primary, +.not-found-secondary { + align-items: center; + border: 1px solid transparent; + border-radius: 0.95rem; + display: inline-flex; + font-size: 0.95rem; + font-weight: 600; + justify-content: center; + min-height: 2.9rem; + padding: 0.85rem 1.25rem; + text-decoration: none; + transition: + transform 0.18s ease, + box-shadow 0.18s ease, + background-color 0.18s ease, + border-color 0.18s ease; +} + +.not-found-primary { + background: var(--gradient-not-found-primary); + box-shadow: var(--shadow-brand-action); + color: var(--color-text-inverse); +} + +.not-found-primary:hover, +.not-found-secondary:hover { + transform: translateY(-1px); +} + +.not-found-secondary-form { + margin: 0; +} + +.not-found-secondary { + background: var(--color-surface-elevated-soft); + border-color: var(--color-border-overlay-strong); + color: var(--color-text-overlay); + cursor: pointer; +} + +.not-found-meta { + align-items: center; + color: var(--color-text-disabled); + display: flex; + font-size: 0.95rem; + gap: 0.5rem; + margin-top: 1.5rem; +} + +.not-found-meta strong { + color: var(--color-surface-muted-inverse); +} + +.tasks-section { + background: var(--color-surface-default); + border: 1px solid var(--color-border-subtle); + border-radius: 1rem; + overflow: hidden; +} + +.tasks-section-header { + align-items: center; + border-bottom: 1px solid var(--color-border-muted); + display: flex; + justify-content: space-between; + padding: 1.2rem 1rem; +} + +.tasks-add-button { + align-items: center; + background: var(--color-surface-default); + border: 1px solid var(--color-border-muted); + border-radius: 0.7rem; + color: var(--color-text-secondary); + cursor: pointer; + display: inline-flex; + font-weight: 500; + gap: 0.5rem; + min-height: 2.75rem; + padding: 0.7rem 1rem; +} + +.task-list { + display: flex; + flex-direction: column; +} + +.task-row { + align-items: center; + border-bottom: 1px solid var(--color-border-muted); + cursor: pointer; + display: flex; + gap: 0.75rem; + padding: 0.9rem 1rem; + transition: background-color 0.2s ease; +} + +.task-row:hover { + background: var(--color-surface-neutral-hover); +} + +.task-check { + align-items: center; + background: var(--color-surface-default); + border: 2px solid var(--color-border-strong); + border-radius: 999px; + color: var(--color-text-inverse); + cursor: pointer; + display: inline-flex; + flex-shrink: 0; + height: 2rem; + justify-content: center; + width: 2rem; +} + +.task-check.is-complete { + background: var(--color-text-brand-strong); + border-color: var(--color-text-brand-strong); +} + +.task-body { + flex: 1; + min-width: 0; +} + +.task-body p { + color: var(--color-surface-muted-inverse); + font-size: 0.95rem; + font-weight: 500; + margin: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.task-row.is-complete .task-body p { + color: var(--color-text-faint); + text-decoration: line-through; +} + +.task-meta { + align-items: center; + color: var(--color-text-muted); + display: flex; + flex-wrap: wrap; + font-size: 0.75rem; + gap: 0.45rem; + margin-top: 0.3rem; +} + +.task-project-badge { + align-items: center; + border-radius: 0.35rem; + color: var(--color-text-inverse); + display: inline-flex; + font-size: 0.5rem; + font-weight: 700; + height: 1rem; + justify-content: center; + width: 1rem; +} + +.task-date { + white-space: nowrap; +} + +.home-card { + backdrop-filter: blur(12px); + background: var(--card); + border: 1px solid var(--border); + border-radius: 1rem; + box-shadow: var(--shadow-auth-card); + max-width: 28rem; + padding: 2rem; + text-align: center; + width: 100%; +} + +.home-card h1 { + margin: 0 0 0.75rem; +} + +.home-card p { + color: var(--muted-foreground); + margin: 0; +} + +.home-logo { + display: block; + height: 4rem; + margin: 0 auto 1rem; + object-fit: contain; + width: 4rem; +} + +.logout-form { + margin-top: 1.5rem; +} + +.logout-button { + max-width: 14rem; + width: 100%; +} + +@media (max-width: 980px) { + .dashboard-shell { + grid-template-columns: 1fr; + } + + .sidebar-nav-shell { + border-bottom: 1px solid var(--color-border-panel); + border-right: 0; + box-shadow: var(--shadow-surface-hover); + height: auto; + position: static; + } + + .overview-actions, + .project-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 720px) { + .dashboard-main { + padding: 1rem; + } + + .overview-header-row, + .tasks-section-header { + align-items: flex-start; + flex-direction: column; + } + + .overview-actions, + .project-grid { + grid-template-columns: 1fr; + } + + .task-row { + align-items: flex-start; + flex-wrap: wrap; + } + + .task-status { + margin-left: 2.75rem; + } + + .not-found-surface { + border-radius: 1.5rem; + padding: 2rem; + } + + .not-found-actions { + flex-direction: column; + } + + .not-found-primary, + .not-found-secondary, + .not-found-secondary-form { + width: 100%; + } + + .app-section-page { + padding: 1rem; + } + + .app-section-surface { + padding: 1.5rem; + } +} + +@keyframes move-right-slow { + from { transform: translateX(-6rem); } + to { transform: translateX(calc(100vw + 6rem)); } +} + +@keyframes move-right-medium { + from { transform: translateX(-5rem); } + to { transform: translateX(calc(100vw + 5rem)); } +} + +@keyframes move-right-fast { + from { transform: translateX(-7rem); } + to { transform: translateX(calc(100vw + 7rem)); } +} + +@keyframes move-down-slow { + from { transform: translateY(-6rem); } + to { transform: translateY(calc(100vh + 6rem)); } +} + +@keyframes move-down-medium { + from { transform: translateY(-5rem); } + to { transform: translateY(calc(100vh + 5rem)); } +} + +@keyframes move-diagonal-1 { + from { transform: translate(-4rem, -4rem); } + to { transform: translate(52vw, 70vh); } +} + +@keyframes move-diagonal-2 { + from { transform: translate(0, -4rem); } + to { transform: translate(-18vw, 76vh); } +} + +@keyframes move-diagonal-3 { + from { transform: translate(0, -4rem); } + to { transform: translate(12vw, 72vh); } +} + +@keyframes orbit-1 { + from { transform: translate(-50%, -50%) rotate(0deg) translateX(11rem); } + to { transform: translate(-50%, -50%) rotate(360deg) translateX(11rem); } +} + +@keyframes orbit-2 { + from { transform: translate(-50%, -50%) rotate(0deg) translateX(7rem); } + to { transform: translate(-50%, -50%) rotate(-360deg) translateX(7rem); } +} + +@keyframes orbit-3 { + from { transform: translate(-50%, -50%) rotate(0deg) translateX(5rem); } + to { transform: translate(-50%, -50%) rotate(360deg) translateX(5rem); } +} + +@keyframes orbit-4 { + from { transform: translate(-50%, -50%) rotate(0deg) translateX(15rem); } + to { transform: translate(-50%, -50%) rotate(360deg) translateX(15rem); } +} + +@keyframes orbit-5 { + from { transform: translate(-50%, -50%) rotate(0deg) translateX(8rem); } + to { transform: translate(-50%, -50%) rotate(-360deg) translateX(8rem); } +} + +@keyframes zigzag-1 { + 0% { transform: translateX(-6rem) translateY(0); } + 25% { transform: translateX(25vw) translateY(-3rem); } + 50% { transform: translateX(50vw) translateY(3rem); } + 75% { transform: translateX(75vw) translateY(-2rem); } + 100% { transform: translateX(calc(100vw + 6rem)) translateY(1rem); } +} + +@keyframes zigzag-2 { + 0% { transform: translateX(-5rem) translateY(0); } + 25% { transform: translateX(25vw) translateY(2rem); } + 50% { transform: translateX(50vw) translateY(-3rem); } + 75% { transform: translateX(75vw) translateY(1.5rem); } + 100% { transform: translateX(calc(100vw + 5rem)) translateY(0); } +} + +@keyframes zigzag-3 { + 0% { transform: translateX(-7rem) translateY(0); } + 20% { transform: translateX(20vw) translateY(-4rem); } + 40% { transform: translateX(40vw) translateY(4rem); } + 60% { transform: translateX(60vw) translateY(-2rem); } + 80% { transform: translateX(80vw) translateY(3rem); } + 100% { transform: translateX(calc(100vw + 7rem)) translateY(0); } +} + +@keyframes spiral-1 { + 0% { transform: translate(0, 0) rotate(0deg) scale(0.6); } + 100% { transform: translate(90vw, 90vh) rotate(360deg) scale(1.3); } +} + +@keyframes spiral-2 { + 0% { transform: translate(0, 0) rotate(0deg) scale(1.4); } + 100% { transform: translate(-70vw, 90vh) rotate(-360deg) scale(0.7); } +} + +@keyframes float-random-1 { + 0%, 100% { transform: translate(0, 0); } + 50% { transform: translate(1.4rem, -1rem); } +} + +@keyframes float-random-2 { + 0%, 100% { transform: translate(0, 0); } + 50% { transform: translate(-1.2rem, 1.1rem); } +} + +@keyframes float-random-3 { + 0%, 100% { transform: translate(0, 0); } + 50% { transform: translate(0.9rem, -1.4rem); } +} + +@keyframes float-random-4 { + 0%, 100% { transform: translate(0, 0); } + 50% { transform: translate(-1rem, 0.8rem); } +} + +@keyframes wave-1 { + from { transform: translateX(-4rem); } + to { transform: translateX(calc(100vw + 4rem)); } +} + +@keyframes wave-2 { + from { transform: translateX(-5rem); } + to { transform: translateX(calc(100vw + 5rem)); } +} + +@keyframes wave-3 { + from { transform: translateX(-4rem); } + to { transform: translateX(calc(100vw + 4rem)); } +} + +@keyframes wave-4 { + from { transform: translateX(-6rem); } + to { transform: translateX(calc(100vw + 6rem)); } +} + +@keyframes corner-shoot-1 { + from { transform: translate(0, 0); } + to { transform: translate(110vw, 110vh); } +} + +@keyframes corner-shoot-2 { + from { transform: translate(0, 0); } + to { transform: translate(-110vw, 110vh); } +} + +@keyframes corner-shoot-3 { + from { transform: translate(0, 0); } + to { transform: translate(110vw, -110vh); } +} + +@keyframes corner-shoot-4 { + from { transform: translate(0, 0); } + to { transform: translate(-110vw, -110vh); } +} + +@keyframes bounce-ball-1 { + 0%, 100% { transform: translateY(0) translateX(0); } + 50% { transform: translateY(-5rem) translateX(2rem); } +} + +@keyframes bounce-ball-2 { + 0%, 100% { transform: translateY(0) translateX(0); } + 50% { transform: translateY(4rem) translateX(-2rem); } +} + +@keyframes bounce-ball-3 { + 0%, 100% { transform: translateY(0) translateX(0); } + 50% { transform: translateY(-4rem) translateX(1.5rem); } +} + +@keyframes spin-slow { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +@keyframes spin-reverse { + from { transform: rotate(0deg); } + to { transform: rotate(-360deg); } +} + +@keyframes bounce-gentle { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-0.6rem); } +} + +@keyframes pulse-gentle { + 0%, 100% { transform: scale(1); opacity: 1; } + 50% { transform: scale(1.08); opacity: 0.85; } +} + +@keyframes wiggle { + 0%, 100% { transform: rotate(0deg); } + 25% { transform: rotate(-4deg); } + 75% { transform: rotate(4deg); } +} + +@keyframes float-gentle { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-0.8rem); } +} + +@keyframes scale-gentle { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.12); } +} + +@keyframes rotate-gentle { + 0%, 100% { transform: rotate(0deg); } + 50% { transform: rotate(12deg); } +} + +@keyframes bounce-soft { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-0.9rem); } +} + +@keyframes sway { + 0%, 100% { transform: translateX(0); } + 50% { transform: translateX(0.8rem); } +} + +@keyframes spin-fast { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +@keyframes pulse-fast { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.2); } +} + +@keyframes wobble { + 0%, 100% { transform: translateX(0); } + 15% { transform: translateX(-0.35rem) rotate(-5deg); } + 30% { transform: translateX(0.25rem) rotate(4deg); } + 45% { transform: translateX(-0.2rem) rotate(-2deg); } + 60% { transform: translateX(0.12rem) rotate(1deg); } +} + +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-0.12rem); } + 75% { transform: translateX(0.12rem); } +} + +@keyframes bounce-crazy { + 0%, 100% { transform: translateY(0) scale(1); } + 25% { transform: translateY(-0.5rem) scale(1.05); } + 75% { transform: translateY(0.2rem) scale(0.98); } +} + +@keyframes spin-wobble { + 0% { transform: rotate(0deg) scale(1); } + 50% { transform: rotate(180deg) scale(1.12); } + 100% { transform: rotate(360deg) scale(1); } +} + +@keyframes flip { + 0%, 100% { transform: rotateY(0deg); } + 50% { transform: rotateY(180deg); } +} + +@keyframes twirl { + 0%, 100% { transform: rotate(0deg) scale(1); } + 50% { transform: rotate(180deg) scale(1.15); } +} + +@keyframes dance { + 0%, 100% { transform: translateY(0) rotate(0deg); } + 25% { transform: translateY(-0.3rem) rotate(-6deg); } + 75% { transform: translateY(0.3rem) rotate(6deg); } +} + +@keyframes jiggle { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-0.18rem); } + 50% { transform: translateX(0.18rem); } + 75% { transform: translateX(-0.1rem); } +} + +@keyframes vibrate { + 0%, 100% { transform: translate(0); } + 20% { transform: translate(-1px, 1px); } + 40% { transform: translate(1px, -1px); } + 60% { transform: translate(-1px, -1px); } + 80% { transform: translate(1px, 1px); } +} + +@keyframes swing { + 0%, 100% { transform: rotate(0deg); } + 50% { transform: rotate(10deg); } +} + +@keyframes pendulum { + 0%, 100% { transform: rotate(-8deg); } + 50% { transform: rotate(8deg); } +} + +@keyframes elastic { + 0%, 100% { transform: scale(1); } + 30% { transform: scale(1.15, 0.9); } + 60% { transform: scale(0.95, 1.08); } +} + +@keyframes rubber { + 0%, 100% { transform: scale(1); } + 35% { transform: scale(1.2, 0.9); } + 65% { transform: scale(0.9, 1.15); } +} + +@keyframes rocket { + 0%, 100% { transform: translateY(0) rotate(-8deg); } + 50% { transform: translateY(-0.8rem) rotate(-12deg); } +} + +@keyframes comet { + 0%, 100% { transform: translateX(0) rotate(12deg); } + 50% { transform: translateX(0.8rem) rotate(18deg); } +} + +@keyframes meteor { + 0%, 100% { transform: translateY(0) rotate(8deg); } + 50% { transform: translateY(-0.7rem) rotate(14deg); } +} + +@keyframes blast { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.2) rotate(10deg); } +} + +@keyframes spin-bounce { + 0%, 100% { transform: rotate(0deg) translateY(0); } + 50% { transform: rotate(180deg) translateY(-0.5rem); } +} + +@keyframes flip-bounce { + 0%, 100% { transform: rotateY(0deg) translateY(0); } + 50% { transform: rotateY(180deg) translateY(-0.5rem); } +} + +@keyframes scale-bounce { + 0%, 100% { transform: scale(1) translateY(0); } + 50% { transform: scale(1.14) translateY(-0.55rem); } +} + +.animate-move-right-slow { animation: move-right-slow 25s linear infinite; } +.animate-move-right-medium { animation: move-right-medium 20s linear infinite; } +.animate-move-right-fast { animation: move-right-fast 15s linear infinite; } +.animate-move-down-slow { animation: move-down-slow 30s linear infinite; } +.animate-move-down-medium { animation: move-down-medium 25s linear infinite; } +.animate-move-diagonal-1 { animation: move-diagonal-1 35s linear infinite; } +.animate-move-diagonal-2 { animation: move-diagonal-2 28s linear infinite; } +.animate-move-diagonal-3 { animation: move-diagonal-3 32s linear infinite; } +.animate-orbit-1 { animation: orbit-1 20s linear infinite; } +.animate-orbit-2 { animation: orbit-2 25s linear infinite reverse; } +.animate-orbit-3 { animation: orbit-3 15s linear infinite; } +.animate-orbit-4 { animation: orbit-4 22s linear infinite; } +.animate-orbit-5 { animation: orbit-5 18s linear infinite; } +.animate-zigzag-1 { animation: zigzag-1 18s linear infinite; } +.animate-zigzag-2 { animation: zigzag-2 22s linear infinite; } +.animate-zigzag-3 { animation: zigzag-3 16s linear infinite; } +.animate-spiral-1 { animation: spiral-1 30s linear infinite; } +.animate-spiral-2 { animation: spiral-2 25s linear infinite; } +.animate-float-random-1 { animation: float-random-1 8s ease-in-out infinite; } +.animate-float-random-2 { animation: float-random-2 10s ease-in-out infinite; } +.animate-float-random-3 { animation: float-random-3 12s ease-in-out infinite; } +.animate-float-random-4 { animation: float-random-4 9s ease-in-out infinite; } +.animate-wave-1 { animation: wave-1 20s linear infinite; } +.animate-wave-2 { animation: wave-2 24s linear infinite; } +.animate-wave-3 { animation: wave-3 18s linear infinite; } +.animate-wave-4 { animation: wave-4 26s linear infinite; } +.animate-corner-shoot-1 { animation: corner-shoot-1 15s linear infinite; } +.animate-corner-shoot-2 { animation: corner-shoot-2 18s linear infinite; } +.animate-corner-shoot-3 { animation: corner-shoot-3 20s linear infinite; } +.animate-corner-shoot-4 { animation: corner-shoot-4 16s linear infinite; } +.animate-bounce-ball-1 { animation: bounce-ball-1 12s ease-in-out infinite; } +.animate-bounce-ball-2 { animation: bounce-ball-2 14s ease-in-out infinite; } +.animate-bounce-ball-3 { animation: bounce-ball-3 10s ease-in-out infinite; } +.animate-spin-slow { animation: spin-slow 8s linear infinite; } +.animate-spin-reverse { animation: spin-reverse 6s linear infinite; } +.animate-bounce-gentle { animation: bounce-gentle 3s ease-in-out infinite; } +.animate-bounce-soft { animation: bounce-soft 4s ease-in-out infinite; } +.animate-pulse-gentle { animation: pulse-gentle 4s ease-in-out infinite; } +.animate-wiggle { animation: wiggle 2s ease-in-out infinite; } +.animate-float-gentle { animation: float-gentle 5s ease-in-out infinite; } +.animate-scale-gentle { animation: scale-gentle 6s ease-in-out infinite; } +.animate-rotate-gentle { animation: rotate-gentle 8s ease-in-out infinite; } +.animate-sway { animation: sway 3s ease-in-out infinite; } +.animate-spin-fast { animation: spin-fast 2s linear infinite; } +.animate-pulse-fast { animation: pulse-fast 1.5s ease-in-out infinite; } +.animate-wobble { animation: wobble 2s ease-in-out infinite; } +.animate-shake { animation: shake 0.5s ease-in-out infinite; } +.animate-bounce-crazy { animation: bounce-crazy 1s ease-in-out infinite; } +.animate-spin-wobble { animation: spin-wobble 4s ease-in-out infinite; } +.animate-flip { animation: flip 3s ease-in-out infinite; } +.animate-twirl { animation: twirl 5s ease-in-out infinite; } +.animate-dance { animation: dance 3s ease-in-out infinite; } +.animate-jiggle { animation: jiggle 1s ease-in-out infinite; } +.animate-vibrate { animation: vibrate 0.3s ease-in-out infinite; } +.animate-swing { animation: swing 2.8s ease-in-out infinite; } +.animate-pendulum { animation: pendulum 2.4s ease-in-out infinite; } +.animate-elastic { animation: elastic 2.2s ease-in-out infinite; } +.animate-rubber { animation: rubber 2.5s ease-in-out infinite; } +.animate-rocket { animation: rocket 1.8s ease-in-out infinite; } +.animate-comet { animation: comet 2s ease-in-out infinite; } +.animate-meteor { animation: meteor 1.7s ease-in-out infinite; } +.animate-blast { animation: blast 2.2s ease-in-out infinite; } +.animate-spin-bounce { animation: spin-bounce 2.4s ease-in-out infinite; } +.animate-flip-bounce { animation: flip-bounce 2.6s ease-in-out infinite; } +.animate-scale-bounce { animation: scale-bounce 2.1s ease-in-out infinite; } + +@media (max-width: 640px) { + .login-screen { + padding: 2rem 1rem; + } + + .auth-card-shell { + padding: 1.25rem; + } +} diff --git a/go-backend/internal/web/ui/badge.css b/go-backend/internal/web/ui/badge.css new file mode 100644 index 0000000..0adaed4 --- /dev/null +++ b/go-backend/internal/web/ui/badge.css @@ -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); +} diff --git a/go-backend/internal/web/ui/base.css b/go-backend/internal/web/ui/base.css new file mode 100644 index 0000000..6c6df95 --- /dev/null +++ b/go-backend/internal/web/ui/base.css @@ -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; +} diff --git a/go-backend/internal/web/ui/button.css b/go-backend/internal/web/ui/button.css new file mode 100644 index 0000000..a7e912c --- /dev/null +++ b/go-backend/internal/web/ui/button.css @@ -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); +} diff --git a/go-backend/internal/web/ui/card.css b/go-backend/internal/web/ui/card.css new file mode 100644 index 0000000..57dae0f --- /dev/null +++ b/go-backend/internal/web/ui/card.css @@ -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; +} diff --git a/go-backend/internal/web/ui/catalog/catalog.css b/go-backend/internal/web/ui/catalog/catalog.css new file mode 100644 index 0000000..bfa09c1 --- /dev/null +++ b/go-backend/internal/web/ui/catalog/catalog.css @@ -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; +} diff --git a/go-backend/internal/web/ui/empty-state.css b/go-backend/internal/web/ui/empty-state.css new file mode 100644 index 0000000..b361f0a --- /dev/null +++ b/go-backend/internal/web/ui/empty-state.css @@ -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; +} diff --git a/go-backend/internal/web/ui/form-field.css b/go-backend/internal/web/ui/form-field.css new file mode 100644 index 0000000..922a3c8 --- /dev/null +++ b/go-backend/internal/web/ui/form-field.css @@ -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; +} diff --git a/go-backend/internal/web/ui/icon-button.css b/go-backend/internal/web/ui/icon-button.css new file mode 100644 index 0000000..a60bdcc --- /dev/null +++ b/go-backend/internal/web/ui/icon-button.css @@ -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; +} diff --git a/go-backend/internal/web/ui/input.css b/go-backend/internal/web/ui/input.css new file mode 100644 index 0000000..222d7fa --- /dev/null +++ b/go-backend/internal/web/ui/input.css @@ -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; +} diff --git a/go-backend/internal/web/ui/modal.css b/go-backend/internal/web/ui/modal.css new file mode 100644 index 0000000..854e1cd --- /dev/null +++ b/go-backend/internal/web/ui/modal.css @@ -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; +} diff --git a/go-backend/internal/web/ui/spacing.css b/go-backend/internal/web/ui/spacing.css new file mode 100644 index 0000000..2d0e782 --- /dev/null +++ b/go-backend/internal/web/ui/spacing.css @@ -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; +} diff --git a/go-backend/internal/web/ui/table.css b/go-backend/internal/web/ui/table.css new file mode 100644 index 0000000..292f192 --- /dev/null +++ b/go-backend/internal/web/ui/table.css @@ -0,0 +1,10 @@ +.ui-table-shell { + overflow-x: auto; + width: 100%; +} + +.ui-table { + border-collapse: collapse; + min-width: 100%; + width: 100%; +} diff --git a/go-backend/internal/web/ui/textarea.css b/go-backend/internal/web/ui/textarea.css new file mode 100644 index 0000000..7142068 --- /dev/null +++ b/go-backend/internal/web/ui/textarea.css @@ -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; +} diff --git a/go-backend/internal/web/ui/ui_test.go b/go-backend/internal/web/ui/ui_test.go index 645d32a..4d56475 100644 --- a/go-backend/internal/web/ui/ui_test.go +++ b/go-backend/internal/web/ui/ui_test.go @@ -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`, diff --git a/go-backend/internal/web/views/tablos.templ b/go-backend/internal/web/views/tablos.templ index cbe9fd4..19eb1a3 100644 --- a/go-backend/internal/web/views/tablos.templ +++ b/go-backend/internal/web/views/tablos.templ @@ -183,7 +183,7 @@ templ TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) { Label: tablo.StatusLabel, Variant: badgeVariantForTone(tablo.StatusTone), }) -