diff --git a/backend/internal/web/ui/badge.css b/backend/internal/web/ui/badge.css index fc03a02..ab9b885 100644 --- a/backend/internal/web/ui/badge.css +++ b/backend/internal/web/ui/badge.css @@ -1,28 +1,42 @@ -/* badge.css — Phase 1 ships info / success / danger. - * `warning` is declared in variants.go for forward compatibility but has no - * CSS rule in Phase 1 — it lands when a page first needs it. - */ +/* badge.css — pill-shape badges ported from go-backend. */ .ui-badge { - display: inline-block; - padding: 0.125rem 0.5rem; + border: 1px solid transparent; + border-radius: 999px; + display: inline-flex; font-size: 0.75rem; font-weight: 600; - border-radius: 0.25rem; - line-height: 1.25; + line-height: 1.2; + padding: 0.3rem 0.75rem; } .ui-badge-info { - background-color: #dbeafe; - color: #1d4ed8; + 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-color: #dcfce7; - color: #16a34a; + 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-color: #fee2e2; - color: #dc2626; + background: var(--color-status-danger-soft-bg); + border-color: var(--color-status-danger-soft-border); + color: var(--color-status-danger-foreground); +} + +/* Primary variant — new in Phase 13, D-CA02 */ +.ui-badge-primary { + background: var(--color-surface-brand-soft); + border-color: rgba(128, 78, 236, 0.3); + color: var(--color-text-brand); } diff --git a/backend/internal/web/ui/button.css b/backend/internal/web/ui/button.css index f165417..e8f3b40 100644 --- a/backend/internal/web/ui/button.css +++ b/backend/internal/web/ui/button.css @@ -1,180 +1,194 @@ -/* button.css — Phase 1 ships only solid / default / md. - * Codex concern #7: no CSS nesting (`&:hover`). All pseudo-class rules are - * declared as top-level selectors so the file is portable across every - * Tailwind v4 standalone processing mode. +/* button.css — multi-class selector pattern ported from go-backend. + * No CSS nesting: all pseudo-class rules are top-level selectors. */ .ui-button { - display: inline-flex; - align-items: center; - font-family: inherit; - font-size: 1rem; - line-height: 1.25; - border: 1px solid transparent; - cursor: pointer; - user-select: none; - text-decoration: none; + align-items: center; + border: 0; + border-radius: 0.35rem; + 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 { - outline: 2px solid #1d4ed8; - outline-offset: 2px; + box-shadow: 0 0 0 3px var(--color-focus-ring); + outline: none; } -.ui-button.htmx-request { - opacity: 0.6; - pointer-events: none; +.ui-button-sm { + font-size: 0.875rem; + min-height: 40px; + padding: 0.625rem 0.9rem; } -.ui-button-solid-default-md { - display: inline-flex; - align-items: center; - border-radius: 0.375rem; - background-color: #2563eb; - padding: 0.5rem 1rem; - font-size: 1rem; - font-weight: 600; - color: #ffffff; +.ui-button-md { + font-size: 0.95rem; + padding: 0.7rem 1rem; } -.ui-button-solid-default-md:hover { - background-color: #1d4ed8; +.ui-button-lg { + font-size: 1rem; + padding: 0.82rem 1.15rem; } -.ui-button-solid-default-md:focus-visible { - outline: 2px solid #1d4ed8; - outline-offset: 2px; +.ui-button-solid.ui-button-default { + background: var(--color-brand-primary); + color: var(--color-brand-foreground, var(--color-text-inverse)); } -/* Phase 3: Danger (solid) and Neutral-soft button variants. */ -/* Codex concern #7: no CSS nesting — all pseudo-class rules are top-level. */ - -.ui-button-solid-danger-md { - display: inline-flex; - align-items: center; - border-radius: 0.375rem; - background-color: #b91c1c; - padding: 0.5rem 1rem; - font-size: 1rem; - font-weight: 600; - color: #ffffff; - min-height: 44px; +.ui-button-solid.ui-button-default:hover { + background: var(--color-brand-primary-hover); } -.ui-button-solid-danger-md:hover { - background-color: #991b1b; +.ui-button-solid.ui-button-default:active { + background: var(--color-brand-primary-active); } -.ui-button-solid-danger-md:focus-visible { - outline: 2px solid #b91c1c; - outline-offset: 2px; +.ui-button-solid.ui-button-neutral { + background: var(--color-surface-muted); + color: var(--color-text-primary); } -.ui-button-soft-neutral-md { - display: inline-flex; - align-items: center; - border-radius: 0.375rem; - background-color: #f1f5f9; - padding: 0.5rem 1rem; - font-size: 1rem; - font-weight: 400; - color: #334155; - border: 1px solid #e2e8f0; - min-height: 44px; +.ui-button-solid.ui-button-neutral:hover { + background: var(--color-surface-muted-hover); } -.ui-button-soft-neutral-md:hover { - background-color: #e2e8f0; +.ui-button-solid.ui-button-neutral:active { + background: var(--color-surface-muted-active); } -.ui-button-soft-neutral-md:focus-visible { - outline: 2px solid #64748b; - outline-offset: 2px; +.ui-button-solid.ui-button-warning { + background: var(--color-status-warning-strong); + color: var(--color-status-warning-strong-foreground); } -/* Phase 4: Soft-danger button variant for task delete actions. */ -/* Codex concern #7: no CSS nesting — all pseudo-class rules are top-level. */ - -.ui-button-soft-danger-md { - display: inline-flex; - align-items: center; - border-radius: 0.375rem; - background-color: #fee2e2; - padding: 0.5rem 1rem; - font-size: 1rem; - font-weight: 600; - color: #b91c1c; - border: 1px solid #fecaca; - min-height: 44px; +.ui-button-solid.ui-button-warning:hover { + background: var(--color-status-warning-strong-hover); } -.ui-button-soft-danger-md:hover { - background-color: #fecaca; +.ui-button-solid.ui-button-warning:active { + background: var(--color-status-warning-strong-active); } -.ui-button-soft-danger-md:focus-visible { - outline: 2px solid #b91c1c; - outline-offset: 2px; +.ui-button-solid.ui-button-success { + background: var(--color-status-success-strong); + color: var(--color-status-success-strong-foreground); } -/* Phase 8: neutral provider controls for social sign-in. */ - -.auth-provider-stack { - display: grid; - gap: 8px; - margin-bottom: 16px; +.ui-button-solid.ui-button-success:hover { + background: var(--color-status-success-strong-hover); } -.auth-provider-button { - display: inline-flex; - min-height: 44px; - width: 100%; - align-items: center; - justify-content: center; - border-radius: 0.375rem; - border: 1px solid #e2e8f0; - background-color: #ffffff; - padding: 0.625rem 1rem; - color: #0f172a; - font-size: 1rem; - font-weight: 600; - line-height: 1.25; - text-align: center; - text-decoration: none; +.ui-button-solid.ui-button-success:active { + background: var(--color-status-success-strong-active); } -.auth-provider-button:hover { - background-color: #f8fafc; +.ui-button-solid.ui-button-danger { + background: var(--color-status-danger-strong); + color: var(--color-status-danger-strong-foreground); } -.auth-provider-button:focus-visible { - outline: 2px solid #2563eb; - outline-offset: 2px; +.ui-button-solid.ui-button-danger:hover { + background: var(--color-status-danger-strong-hover); } -.auth-provider-button-disabled, -.auth-provider-button-disabled:hover { - background-color: #f1f5f9; - color: #94a3b8; - cursor: not-allowed; +.ui-button-solid.ui-button-danger:active { + background: var(--color-status-danger-strong-active); } -.auth-provider-separator { - display: grid; - grid-template-columns: 1fr auto 1fr; - align-items: center; - gap: 8px; - margin: 16px 0; - color: #64748b; - font-size: 0.875rem; - line-height: 1.4; +.ui-button-soft.ui-button-default { + background: var(--color-surface-brand-soft); + color: var(--color-brand-primary-hover); } -.auth-provider-separator span { - height: 1px; - background-color: #e2e8f0; +.ui-button-soft.ui-button-default:hover { + background: var(--color-surface-brand-soft-hover); } -.auth-provider-separator em { - font-style: normal; +.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); +} + +/* Ghost variant — new in Phase 13, D-CA01 */ +.ui-button-ghost { + background: transparent; + color: var(--color-brand-primary); +} + +.ui-button-ghost:hover { + background: var(--color-surface-brand-soft); +} + +.ui-button-ghost:focus-visible { + box-shadow: 0 0 0 3px var(--color-focus-ring); + outline: none; +} + +.ui-button-soft.ui-button-neutral { + background: var(--color-surface-muted); + color: var(--color-text-secondary); +} + +.ui-button-soft.ui-button-neutral:hover { + background: var(--color-surface-muted-hover); +} + +.ui-button-soft.ui-button-neutral:active { + background: var(--color-surface-muted-active); } diff --git a/backend/internal/web/ui/card.css b/backend/internal/web/ui/card.css index 74dcba6..a3833d3 100644 --- a/backend/internal/web/ui/card.css +++ b/backend/internal/web/ui/card.css @@ -1,8 +1,29 @@ -/* card.css — slate-50 panel with slate-200 border. */ +/* card.css — token-based card with header/body/footer regions ported from go-backend. */ .ui-card { - border-radius: 0.5rem; - border: 1px solid #e2e8f0; - background-color: #f8fafc; - padding: 1.5rem; + 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/backend/internal/web/ui/card.templ b/backend/internal/web/ui/card.templ index 29893a9..6615653 100644 --- a/backend/internal/web/ui/card.templ +++ b/backend/internal/web/ui/card.templ @@ -1,11 +1,24 @@ package ui -// Card is a slate-50 panel with a slate-200 border. Children are rendered -// inside via templ's child-content syntax: `@ui.Card(nil) {
...
}`. -// -// `attrs` is a pass-through for arbitrary attributes (id, data-*, hx-*). -templ Card(attrs templ.Attributes) { -x
") - ctx := templ.WithChildren(context.Background(), child) - out := render(t, ctx, Card(nil)) - - if !strings.Contains(out, `x
") { - t.Errorf("output missing child markup; got: %s", out) - } -} func TestBadge_InfoVariant(t *testing.T) { out := render(t, context.Background(), Badge(BadgeProps{Label: "OK", Variant: BadgeVariantInfo})) @@ -200,3 +187,42 @@ func TestSpaceYClass_LG(t *testing.T) { t.Errorf("SpaceYClass(SpacingStepLG) = %q; want %q", got, want) } } + +// textComponent is a test helper that wraps a raw string as a templ.Component, +// used for asserting typed CardProps fields (Header/Body/Footer). +func textComponent(text string) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error { + _, err := w.Write([]byte(text)) + return err + }) +} + +// Phase 13 Plan 02 — typed Card API and Badge primary variant tests (TDD RED) + +func TestCard_RendersTypedRegions(t *testing.T) { + out := render(t, context.Background(), Card(CardProps{ + Header: textComponent("header"), + Body: textComponent("body"), + })) + for _, want := range []string{ + `ui-card-header`, + `header`, + `ui-card-body`, + `body`, + } { + if !strings.Contains(out, want) { + t.Fatalf("expected %q in %q", want, out) + } + } + // nil footer must not emit ui-card-footer + if strings.Contains(out, "ui-card-footer") { + t.Errorf("expected no ui-card-footer when Footer is nil; got: %s", out) + } +} + +func TestBadge_PrimaryVariant(t *testing.T) { + out := render(t, context.Background(), Badge(BadgeProps{Label: "new", Variant: BadgeVariantPrimary})) + if !strings.Contains(out, "ui-badge-primary") { + t.Errorf("expected ui-badge-primary in output; got: %s", out) + } +} diff --git a/backend/templates/auth_login.templ b/backend/templates/auth_login.templ index accb1b8..5f10a02 100644 --- a/backend/templates/auth_login.templ +++ b/backend/templates/auth_login.templ @@ -8,17 +8,19 @@ import "backend/internal/web/ui" templ LoginPage(form LoginForm, errs LoginErrors, csrfToken string, providers AuthProviderButtons) { @Layout("Sign in", nil, csrfToken) {