feat(13-02): replace CSS files, migrate card.templ to typed API, update all template hardcodes
- button.css: replaced with go-backend multi-class selector version + ghost variant rules
- badge.css: replaced with go-backend pill-shape version + primary variant
- card.css: replaced with go-backend token-based header/body/footer version
- card.templ: migrated from children passthrough to typed CardProps{Header/Body/Footer}
- ui_test.go: rewrote TestCard_RendersChildren -> TestCard_RendersTypedRegions; added TestBadge_PrimaryVariant; added textComponent helper + io import
- auth_login.templ, auth_signup.templ: migrated Card usage to typed CardProps API
- tablos.templ: migrated TabloCard to typed CardProps API with extracted tabloCardBody
- planning.templ, tasks.templ, events.templ, etapes.templ: all compound button class strings updated to multi-class pattern
- go test ./... passes (all packages green)
- just generate succeeds
This commit is contained in:
parent
66f23bba77
commit
a30a6f9088
12 changed files with 318 additions and 222 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) { <p>...</p> }`.
|
||||
//
|
||||
// `attrs` is a pass-through for arbitrary attributes (id, data-*, hx-*).
|
||||
templ Card(attrs templ.Attributes) {
|
||||
<section class="ui-card" { attrs... }>
|
||||
{ children... }
|
||||
// CardProps is the input to the Card templ component.
|
||||
// Header, Body, and Footer are optional typed regions — nil fields are omitted
|
||||
// from the rendered output (no empty wrapper divs).
|
||||
type CardProps struct {
|
||||
Header templ.Component
|
||||
Body templ.Component
|
||||
Footer templ.Component
|
||||
}
|
||||
|
||||
templ Card(props CardProps) {
|
||||
<section class="ui-card">
|
||||
if props.Header != nil {
|
||||
<div class="ui-card-header">@props.Header</div>
|
||||
}
|
||||
if props.Body != nil {
|
||||
<div class="ui-card-body">@props.Body</div>
|
||||
}
|
||||
if props.Footer != nil {
|
||||
<div class="ui-card-footer">@props.Footer</div>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package ui
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
|
|
@ -67,20 +68,6 @@ func TestButton_ExplicitTypeSubmit(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// Card injects children via templ.WithChildren. The test feeds a raw child
|
||||
// component through context and asserts the rendered wrapper + child content.
|
||||
func TestCard_RendersChildren(t *testing.T) {
|
||||
child := templ.Raw("<p>x</p>")
|
||||
ctx := templ.WithChildren(context.Background(), child)
|
||||
out := render(t, ctx, Card(nil))
|
||||
|
||||
if !strings.Contains(out, `<section class="ui-card"`) {
|
||||
t.Errorf("output missing <section class=\"ui-card\">; got: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "<p>x</p>") {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,17 +8,19 @@ import "backend/internal/web/ui"
|
|||
templ LoginPage(form LoginForm, errs LoginErrors, csrfToken string, providers AuthProviderButtons) {
|
||||
@Layout("Sign in", nil, csrfToken) {
|
||||
<div class="flex min-h-[60vh] items-start justify-center pt-16">
|
||||
@ui.Card(nil) {
|
||||
<div class="w-full max-w-sm px-6 py-8">
|
||||
<h1 class="mb-6 text-2xl font-semibold">Sign in to your account</h1>
|
||||
@AuthProviderButtonsBlock(providers)
|
||||
@LoginFormFragment(form, errs, csrfToken)
|
||||
</div>
|
||||
}
|
||||
@ui.Card(ui.CardProps{Body: loginCardBody(form, errs, csrfToken, providers)})
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ loginCardBody(form LoginForm, errs LoginErrors, csrfToken string, providers AuthProviderButtons) {
|
||||
<div class="w-full max-w-sm px-6 py-8">
|
||||
<h1 class="mb-6 text-2xl font-semibold">Sign in to your account</h1>
|
||||
@AuthProviderButtonsBlock(providers)
|
||||
@LoginFormFragment(form, errs, csrfToken)
|
||||
</div>
|
||||
}
|
||||
|
||||
// LoginFormFragment is the bare form used for HTMX swaps.
|
||||
// hx-post targets this component itself so the form can be replaced inline
|
||||
// on validation failure (D-19, D-20).
|
||||
|
|
|
|||
|
|
@ -8,17 +8,19 @@ import "backend/internal/web/ui"
|
|||
templ SignupPage(form SignupForm, errs SignupErrors, csrfToken string, providers AuthProviderButtons) {
|
||||
@Layout("Sign up", nil, csrfToken) {
|
||||
<div class="flex min-h-[60vh] items-start justify-center pt-16">
|
||||
@ui.Card(nil) {
|
||||
<div class="w-full max-w-sm px-6 py-8">
|
||||
<h1 class="mb-6 text-2xl font-semibold">Create your account</h1>
|
||||
@AuthProviderButtonsBlock(providers)
|
||||
@SignupFormFragment(form, errs, csrfToken)
|
||||
</div>
|
||||
}
|
||||
@ui.Card(ui.CardProps{Body: signupCardBody(form, errs, csrfToken, providers)})
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ signupCardBody(form SignupForm, errs SignupErrors, csrfToken string, providers AuthProviderButtons) {
|
||||
<div class="w-full max-w-sm px-6 py-8">
|
||||
<h1 class="mb-6 text-2xl font-semibold">Create your account</h1>
|
||||
@AuthProviderButtonsBlock(providers)
|
||||
@SignupFormFragment(form, errs, csrfToken)
|
||||
</div>
|
||||
}
|
||||
|
||||
// SignupFormFragment is the bare form used for HTMX swaps.
|
||||
// hx-post targets this component itself so the form can be replaced inline
|
||||
// on validation failure (D-19, D-25).
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ templ EtapeStrip(tabloID uuid.UUID, etapes []sqlc.Etape, counts EtapeTaskCounts,
|
|||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="ui-button ui-button-soft-neutral-md px-2"
|
||||
class="ui-button ui-button-soft ui-button-neutral ui-button-md px-2"
|
||||
hx-get={ "/tablos/" + tabloID.String() + "/etapes/" + etape.ID.String() + "/edit" }
|
||||
hx-target="#etape-form-slot"
|
||||
hx-swap="innerHTML"
|
||||
|
|
@ -62,7 +62,7 @@ templ EtapeStrip(tabloID uuid.UUID, etapes []sqlc.Etape, counts EtapeTaskCounts,
|
|||
>Edit</button>
|
||||
<button
|
||||
type="button"
|
||||
class="ui-button ui-button-soft-danger-md px-2"
|
||||
class="ui-button ui-button-soft ui-button-danger ui-button-md px-2"
|
||||
hx-get={ "/tablos/" + tabloID.String() + "/etapes/" + etape.ID.String() + "/delete-confirm" }
|
||||
hx-target="#etape-form-slot"
|
||||
hx-swap="innerHTML"
|
||||
|
|
@ -74,7 +74,7 @@ templ EtapeStrip(tabloID uuid.UUID, etapes []sqlc.Etape, counts EtapeTaskCounts,
|
|||
for _, id := range etapeReorderIDs(etapes, index, -1) {
|
||||
<input type="hidden" name="etape_id" value={ id.String() }/>
|
||||
}
|
||||
<button type="submit" class="ui-button ui-button-soft-neutral-md px-2" aria-label={ "Move etape earlier: " + etape.Title }>Up</button>
|
||||
<button type="submit" class="ui-button ui-button-soft ui-button-neutral ui-button-md px-2" aria-label={ "Move etape earlier: " + etape.Title }>Up</button>
|
||||
</form>
|
||||
}
|
||||
if index < len(etapes)-1 {
|
||||
|
|
@ -83,14 +83,14 @@ templ EtapeStrip(tabloID uuid.UUID, etapes []sqlc.Etape, counts EtapeTaskCounts,
|
|||
for _, id := range etapeReorderIDs(etapes, index, 1) {
|
||||
<input type="hidden" name="etape_id" value={ id.String() }/>
|
||||
}
|
||||
<button type="submit" class="ui-button ui-button-soft-neutral-md px-2" aria-label={ "Move etape later: " + etape.Title }>Down</button>
|
||||
<button type="submit" class="ui-button ui-button-soft ui-button-neutral ui-button-md px-2" aria-label={ "Move etape later: " + etape.Title }>Down</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
class="ui-button ui-button-soft-neutral-md flex-shrink-0"
|
||||
class="ui-button ui-button-soft ui-button-neutral ui-button-md flex-shrink-0"
|
||||
hx-get={ "/tablos/" + tabloID.String() + "/etapes/new" }
|
||||
hx-target="#etape-form-slot"
|
||||
hx-swap="innerHTML"
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ templ EventsTabFragment(tablo sqlc.Tablo, calendar EventsCalendar, csrfToken str
|
|||
hx-target="#tab-content"
|
||||
hx-swap="innerHTML"
|
||||
hx-push-url={ EventMonthURL(tablo.ID, calendar.PrevMonth) }
|
||||
class="ui-button ui-button-soft-neutral-md"
|
||||
class="ui-button ui-button-soft ui-button-neutral ui-button-md"
|
||||
aria-label={ "Previous month: " + calendar.PrevMonthLabel }
|
||||
>Previous month</a>
|
||||
<a
|
||||
|
|
@ -29,7 +29,7 @@ templ EventsTabFragment(tablo sqlc.Tablo, calendar EventsCalendar, csrfToken str
|
|||
hx-target="#tab-content"
|
||||
hx-swap="innerHTML"
|
||||
hx-push-url={ EventMonthURL(tablo.ID, calendar.NextMonth) }
|
||||
class="ui-button ui-button-soft-neutral-md"
|
||||
class="ui-button ui-button-soft ui-button-neutral ui-button-md"
|
||||
aria-label={ "Next month: " + calendar.NextMonthLabel }
|
||||
>Next month</a>
|
||||
@ui.Button(ui.ButtonProps{
|
||||
|
|
|
|||
|
|
@ -13,16 +13,16 @@ templ PlanningPage(user *auth.User, csrfToken string, agenda PlanningAgenda) {
|
|||
<p class="mt-1 text-sm text-slate-600">{ agenda.RangeLabel }</p>
|
||||
</div>
|
||||
<nav class="flex flex-wrap items-center gap-2" aria-label="Planning navigation">
|
||||
<a href={ templ.SafeURL(agenda.PrevURL) } class="ui-button ui-button-soft-neutral-md">Previous 14 days</a>
|
||||
<a href={ templ.SafeURL(agenda.PrevURL) } class="ui-button ui-button-soft ui-button-neutral ui-button-md">Previous 14 days</a>
|
||||
<a
|
||||
href={ templ.SafeURL(agenda.TodayURL) }
|
||||
if agenda.ShowingToday {
|
||||
class="ui-button ui-button-soft-neutral-md"
|
||||
class="ui-button ui-button-soft ui-button-neutral ui-button-md"
|
||||
} else {
|
||||
class="ui-button ui-button-solid-default-md"
|
||||
class="ui-button ui-button-solid ui-button-default ui-button-md"
|
||||
}
|
||||
>Today</a>
|
||||
<a href={ templ.SafeURL(agenda.NextURL) } class="ui-button ui-button-soft-neutral-md">Next 14 days</a>
|
||||
<a href={ templ.SafeURL(agenda.NextURL) } class="ui-button ui-button-soft ui-button-neutral ui-button-md">Next 14 days</a>
|
||||
</nav>
|
||||
</div>
|
||||
if len(agenda.Events) == 0 {
|
||||
|
|
|
|||
|
|
@ -67,32 +67,36 @@ templ TablosEmptyState() {
|
|||
// Guards description and color rendering against null pgtype.Text values (Pitfall 6).
|
||||
// Delegates delete-zone rendering to TabloDeleteButtonFragment (single source of truth).
|
||||
templ TabloCard(card TabloCardView, csrfToken string) {
|
||||
@ui.Card(templ.Attributes{"id": "tablo-" + card.Tablo.ID.String()}) {
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h2 class="text-xl font-semibold leading-snug">{ card.Tablo.Title }</h2>
|
||||
@DiscussionUnreadBadge(card.DiscussionUnreadCount)
|
||||
</div>
|
||||
if card.Tablo.Description.Valid && card.Tablo.Description.String != "" {
|
||||
<p class="mt-2 text-base text-slate-600">{ card.Tablo.Description.String }</p>
|
||||
}
|
||||
if card.Tablo.Color.Valid && card.Tablo.Color.String != "" {
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<span
|
||||
class="inline-block w-2.5 h-2.5 rounded-full"
|
||||
style={ "background-color: " + card.Tablo.Color.String }
|
||||
></span>
|
||||
<span class="text-sm text-slate-500">{ card.Tablo.Color.String }</span>
|
||||
</div>
|
||||
}
|
||||
<div id={ "tablo-" + card.Tablo.ID.String() }>
|
||||
@ui.Card(ui.CardProps{Body: tabloCardBody(card, csrfToken)})
|
||||
</div>
|
||||
}
|
||||
|
||||
templ tabloCardBody(card TabloCardView, csrfToken string) {
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h2 class="text-xl font-semibold leading-snug">{ card.Tablo.Title }</h2>
|
||||
@DiscussionUnreadBadge(card.DiscussionUnreadCount)
|
||||
</div>
|
||||
@TabloDeleteButtonFragment(card.Tablo, csrfToken)
|
||||
if card.Tablo.Description.Valid && card.Tablo.Description.String != "" {
|
||||
<p class="mt-2 text-base text-slate-600">{ card.Tablo.Description.String }</p>
|
||||
}
|
||||
if card.Tablo.Color.Valid && card.Tablo.Color.String != "" {
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<span
|
||||
class="inline-block w-2.5 h-2.5 rounded-full"
|
||||
style={ "background-color: " + card.Tablo.Color.String }
|
||||
></span>
|
||||
<span class="text-sm text-slate-500">{ card.Tablo.Color.String }</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<a href={ templ.SafeURL("/tablos/" + card.Tablo.ID.String()) } class="text-sm font-medium text-blue-600 hover:underline">View</a>
|
||||
</div>
|
||||
}
|
||||
@TabloDeleteButtonFragment(card.Tablo, csrfToken)
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<a href={ templ.SafeURL("/tablos/" + card.Tablo.ID.String()) } class="text-sm font-medium text-blue-600 hover:underline">View</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ DiscussionUnreadBadge(count int64) {
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ templ TaskCard(tabloID uuid.UUID, task sqlc.Task, csrfToken string) {
|
|||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="ui-button ui-button-soft-danger-md flex-shrink-0 text-xs"
|
||||
class="ui-button ui-button-soft ui-button-danger ui-button-md flex-shrink-0 text-xs"
|
||||
hx-get={ "/tablos/" + tabloID.String() + "/tasks/" + task.ID.String() + "/delete-confirm" }
|
||||
hx-target="closest .task-card-zone"
|
||||
hx-swap="outerHTML"
|
||||
|
|
@ -372,7 +372,7 @@ templ TaskDeleteConfirmFragment(tabloID uuid.UUID, task sqlc.Task, csrfToken str
|
|||
templ AddTaskTrigger(tabloID uuid.UUID, status sqlc.TaskStatus, csrfToken string, filter EtapeFilter) {
|
||||
<button
|
||||
type="button"
|
||||
class="ui-button ui-button-soft-neutral-md w-full text-left text-sm mt-2"
|
||||
class="ui-button ui-button-soft ui-button-neutral ui-button-md w-full text-left text-sm mt-2"
|
||||
hx-get={ "/tablos/" + tabloID.String() + "/tasks/new?status=" + string(status) + filter.QuerySuffix() }
|
||||
hx-target={ "#add-task-slot-" + string(status) }
|
||||
hx-swap="innerHTML"
|
||||
|
|
|
|||
Loading…
Reference in a new issue