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:
Arthur Belleville 2026-05-16 13:55:30 +02:00
parent 66f23bba77
commit a30a6f9088
No known key found for this signature in database
12 changed files with 318 additions and 222 deletions

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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>
}

View file

@ -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)
}
}

View file

@ -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).

View file

@ -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).

View file

@ -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"

View file

@ -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{

View file

@ -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 {

View file

@ -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) {

View file

@ -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"