feat(01-02): add Button, Card, Badge templ components + CSS

- button.templ: Button(ButtonProps) renders <button type=...> with class
  from ui.ButtonClass(); Attrs spread for hx-* pass-through
- button.css: .ui-button base + .ui-button-solid-default-md variant
  with non-nested :hover and :focus-visible (Codex concern #7)
- card.templ: Card(attrs) accepts children via templ child syntax
- card.css: slate-50 panel, slate-200 border
- badge.templ: Badge(BadgeProps) renders <span class=...>
- badge.css: info / success / danger variants (warning deferred)
This commit is contained in:
Arthur Belleville 2026-05-14 18:46:42 +02:00
parent 1ff8e681da
commit d056b33241
6 changed files with 129 additions and 0 deletions

View file

@ -0,0 +1,28 @@
/* 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.
*/
.ui-badge {
display: inline-block;
padding: 0.125rem 0.5rem;
font-size: 0.75rem;
font-weight: 600;
border-radius: 0.25rem;
line-height: 1.25;
}
.ui-badge-info {
background-color: #dbeafe;
color: #1d4ed8;
}
.ui-badge-success {
background-color: #dcfce7;
color: #16a34a;
}
.ui-badge-danger {
background-color: #fee2e2;
color: #dc2626;
}

View file

@ -0,0 +1,12 @@
package ui
// BadgeProps is the input to the Badge templ component.
type BadgeProps struct {
Label string
Variant BadgeVariant
}
templ Badge(props BadgeProps) {
{{ class := BadgeClass(props.Variant) }}
<span class={ class }>{ props.Label }</span>
}

View file

@ -0,0 +1,47 @@
/* 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.
*/
.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;
}
.ui-button:focus-visible {
outline: 2px solid #1d4ed8;
outline-offset: 2px;
}
.ui-button.htmx-request {
opacity: 0.6;
pointer-events: none;
}
.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-solid-default-md:hover {
background-color: #1d4ed8;
}
.ui-button-solid-default-md:focus-visible {
outline: 2px solid #1d4ed8;
outline-offset: 2px;
}

View file

@ -0,0 +1,23 @@
package ui
// ButtonProps is the input to the Button templ component.
//
// Type defaults to "button" when empty. Attrs is a pass-through for arbitrary
// attributes (notably hx-* HTMX attributes).
type ButtonProps struct {
Label string
Variant ButtonVariant
Tone ButtonTone
Size Size
Type string
Attrs templ.Attributes
}
templ Button(props ButtonProps) {
{{ btnType := props.Type }}
if btnType == "" {
{{ btnType = "button" }}
}
{{ class := ButtonClass(props.Variant, props.Tone, props.Size) }}
<button type={ btnType } class={ class } { props.Attrs... }>{ props.Label }</button>
}

View file

@ -0,0 +1,8 @@
/* card.css — slate-50 panel with slate-200 border. */
.ui-card {
border-radius: 0.5rem;
border: 1px solid #e2e8f0;
background-color: #f8fafc;
padding: 1.5rem;
}

View file

@ -0,0 +1,11 @@
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... }
</section>
}