feat: refresh go-backend button variants and tablos styling

This commit is contained in:
Arthur Belleville 2026-05-10 09:22:22 +02:00
parent a089f35a9b
commit cf64404d25
No known key found for this signature in database
13 changed files with 209 additions and 53 deletions

View file

@ -8,14 +8,21 @@
<link rel="stylesheet" href="../../go-backend/static/styles.css"> <link rel="stylesheet" href="../../go-backend/static/styles.css">
</head> </head>
<body> <body>
<main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link">Tokens</a><a href="./buttons.html" class="catalog-nav-link is-active">Buttons</a><a href="./badges.html" class="catalog-nav-link">Badges</a><a href="./icon-buttons.html" class="catalog-nav-link">Icon Buttons</a><a href="./inputs.html" class="catalog-nav-link">Inputs</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./tables.html" class="catalog-nav-link">Tables</a><a href="./empty-states.html" class="catalog-nav-link">Empty States</a><a href="./cards.html" class="catalog-nav-link">Cards</a></div></nav><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Buttons</h1><p>Primary, secondary, ghost, and destructive actions built from shared templ primitives.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Primary action</h2><p>Used for the main action in a page section or modal footer.</p></div><div class="catalog-example-preview"><button type="button" class="ui-button ui-button-primary ui-button-md">Nouveau projet</button></div><pre class="catalog-example-snippet"><code>@ui.Button(ui.ButtonProps{ <main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link">Tokens</a><a href="./buttons.html" class="catalog-nav-link is-active">Buttons</a><a href="./badges.html" class="catalog-nav-link">Badges</a><a href="./icon-buttons.html" class="catalog-nav-link">Icon Buttons</a><a href="./inputs.html" class="catalog-nav-link">Inputs</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./tables.html" class="catalog-nav-link">Tables</a><a href="./empty-states.html" class="catalog-nav-link">Empty States</a><a href="./cards.html" class="catalog-nav-link">Cards</a></div></nav><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Buttons</h1><p>Primary, secondary, ghost, and destructive actions built from shared templ primitives.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Default solid action</h2><p>Used for the main action in a page section or modal footer.</p></div><div class="catalog-example-preview"><button type="button" class="ui-button ui-button-solid ui-button-default ui-button-md">Nouveau projet</button></div><pre class="catalog-example-snippet"><code>@ui.Button(ui.ButtonProps{
Label: &#34;Nouveau projet&#34;, Label: &#34;Nouveau projet&#34;,
Variant: ui.ButtonVariantPrimary, Variant: ui.ButtonVariantDefault,
Size: ui.SizeMD, Size: ui.SizeMD,
Type: &#34;button&#34;, Type: &#34;button&#34;,
})</code></pre></section><section class="catalog-example"><div class="catalog-example-copy"><h2>Danger action</h2><p>Used for irreversible actions after explicit confirmation.</p></div><div class="catalog-example-preview"><button type="submit" class="ui-button ui-button-danger ui-button-lg">Supprimer</button></div><pre class="catalog-example-snippet"><code>@ui.Button(ui.ButtonProps{ })</code></pre></section><section class="catalog-example"><div class="catalog-example-copy"><h2>Soft warning action</h2><p>Used for inline actions that need emphasis without the weight of a solid button.</p></div><div class="catalog-example-preview"><button type="button" class="ui-button ui-button-soft ui-button-warning ui-button-md">Relancer</button></div><pre class="catalog-example-snippet"><code>@ui.Button(ui.ButtonProps{
Label: &#34;Relancer&#34;,
Variant: ui.ButtonVariantWarning,
Tone: ui.ButtonToneSoft,
Size: ui.SizeMD,
Type: &#34;button&#34;,
})</code></pre></section><section class="catalog-example"><div class="catalog-example-copy"><h2>Soft danger action</h2><p>Used for irreversible actions after explicit confirmation.</p></div><div class="catalog-example-preview"><button type="submit" class="ui-button ui-button-soft ui-button-danger ui-button-lg">Supprimer</button></div><pre class="catalog-example-snippet"><code>@ui.Button(ui.ButtonProps{
Label: &#34;Supprimer&#34;, Label: &#34;Supprimer&#34;,
Variant: ui.ButtonVariantDanger, Variant: ui.ButtonVariantDanger,
Tone: ui.ButtonToneSoft,
Size: ui.SizeLG, Size: ui.SizeLG,
Type: &#34;submit&#34;, Type: &#34;submit&#34;,
})</code></pre></section></div></main> })</code></pre></section></div></main>

View file

@ -8,7 +8,7 @@
<link rel="stylesheet" href="../../go-backend/static/styles.css"> <link rel="stylesheet" href="../../go-backend/static/styles.css">
</head> </head>
<body> <body>
<main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link">Tokens</a><a href="./buttons.html" class="catalog-nav-link">Buttons</a><a href="./badges.html" class="catalog-nav-link">Badges</a><a href="./icon-buttons.html" class="catalog-nav-link">Icon Buttons</a><a href="./inputs.html" class="catalog-nav-link">Inputs</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./tables.html" class="catalog-nav-link">Tables</a><a href="./empty-states.html" class="catalog-nav-link is-active">Empty States</a><a href="./cards.html" class="catalog-nav-link">Cards</a></div></nav><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Empty States</h1><p>Centered fallback messaging with optional icon and action.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Centered empty state</h2><p>Used when a list has no rows yet and the next action should stay obvious.</p></div><div class="catalog-example-preview"><section class="ui-empty-state"><div class="ui-empty-state-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="18" height="18" x="3" y="3" rx="2"></rect> <path d="M3 9h18"></path> <path d="M3 15h18"></path> <path d="M9 3v18"></path> <path d="M15 3v18"></path></svg></div><h3 class="ui-empty-state-title">Aucun projet trouvé</h3><p class="ui-empty-state-description">Créez votre premier projet</p><div class="ui-empty-state-action"><button type="button" class="ui-button ui-button-primary ui-button-md"><span class="ui-button-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"></path> <path d="M12 5v14"></path></svg></span> Nouveau projet</button></div></section></div><pre class="catalog-example-snippet"><code>@ui.EmptyState(ui.EmptyStateProps{ <main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link">Tokens</a><a href="./buttons.html" class="catalog-nav-link">Buttons</a><a href="./badges.html" class="catalog-nav-link">Badges</a><a href="./icon-buttons.html" class="catalog-nav-link">Icon Buttons</a><a href="./inputs.html" class="catalog-nav-link">Inputs</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./tables.html" class="catalog-nav-link">Tables</a><a href="./empty-states.html" class="catalog-nav-link is-active">Empty States</a><a href="./cards.html" class="catalog-nav-link">Cards</a></div></nav><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Empty States</h1><p>Centered fallback messaging with optional icon and action.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Centered empty state</h2><p>Used when a list has no rows yet and the next action should stay obvious.</p></div><div class="catalog-example-preview"><section class="ui-empty-state"><div class="ui-empty-state-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="18" height="18" x="3" y="3" rx="2"></rect> <path d="M3 9h18"></path> <path d="M3 15h18"></path> <path d="M9 3v18"></path> <path d="M15 3v18"></path></svg></div><h3 class="ui-empty-state-title">Aucun projet trouvé</h3><p class="ui-empty-state-description">Créez votre premier projet</p><div class="ui-empty-state-action"><button type="button" class="ui-button ui-button-solid ui-button-default ui-button-md"><span class="ui-button-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"></path> <path d="M12 5v14"></path></svg></span> Nouveau projet</button></div></section></div><pre class="catalog-example-snippet"><code>@ui.EmptyState(ui.EmptyStateProps{
Title: &#34;Aucun projet trouvé&#34;, Title: &#34;Aucun projet trouvé&#34;,
Description: &#34;Créez votre premier projet&#34;, Description: &#34;Créez votre premier projet&#34;,
Icon: ui.UIIcon(&#34;grid3x3&#34;), Icon: ui.UIIcon(&#34;grid3x3&#34;),

View file

@ -8,7 +8,7 @@
<link rel="stylesheet" href="../../go-backend/static/styles.css"> <link rel="stylesheet" href="../../go-backend/static/styles.css">
</head> </head>
<body> <body>
<main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link">Tokens</a><a href="./buttons.html" class="catalog-nav-link">Buttons</a><a href="./badges.html" class="catalog-nav-link">Badges</a><a href="./icon-buttons.html" class="catalog-nav-link">Icon Buttons</a><a href="./inputs.html" class="catalog-nav-link">Inputs</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link is-active">Modals</a><a href="./tables.html" class="catalog-nav-link">Tables</a><a href="./empty-states.html" class="catalog-nav-link">Empty States</a><a href="./cards.html" class="catalog-nav-link">Cards</a></div></nav><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Modals</h1><p>Shared modal shell for focused create, edit, and confirm flows.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Create modal</h2><p>Shared modal shell with a form body and action footer.</p></div><div class="catalog-example-preview"><div class="ui-modal-backdrop"><div class="ui-modal-panel"><div class="ui-modal-header"><h2>Créer un projet</h2></div><div class="ui-modal-body"><div class="ui-form-field"><label for="modal-name" class="ui-form-label">Nom du projet</label> <input id="modal-name" type="text" name="name" value="" placeholder="Nom du projet" class="ui-input"></div></div><div class="ui-modal-actions"><button type="button" class="ui-button ui-button-secondary ui-button-md">Annuler</button><button type="submit" class="ui-button ui-button-primary ui-button-md">Créer le projet</button></div></div></div></div><pre class="catalog-example-snippet"><code>@ui.Modal(ui.ModalProps{ <main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link">Tokens</a><a href="./buttons.html" class="catalog-nav-link">Buttons</a><a href="./badges.html" class="catalog-nav-link">Badges</a><a href="./icon-buttons.html" class="catalog-nav-link">Icon Buttons</a><a href="./inputs.html" class="catalog-nav-link">Inputs</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link is-active">Modals</a><a href="./tables.html" class="catalog-nav-link">Tables</a><a href="./empty-states.html" class="catalog-nav-link">Empty States</a><a href="./cards.html" class="catalog-nav-link">Cards</a></div></nav><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Modals</h1><p>Shared modal shell for focused create, edit, and confirm flows.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Create modal</h2><p>Shared modal shell with a form body and action footer.</p></div><div class="catalog-example-preview"><div class="ui-modal-backdrop"><div class="ui-modal-panel"><div class="ui-modal-header"><h2>Créer un projet</h2></div><div class="ui-modal-body"><div class="ui-form-field"><label for="modal-name" class="ui-form-label">Nom du projet</label> <input id="modal-name" type="text" name="name" value="" placeholder="Nom du projet" class="ui-input"></div></div><div class="ui-modal-actions"><button type="button" class="ui-button ui-button-solid ui-button-neutral ui-button-md">Annuler</button><button type="submit" class="ui-button ui-button-solid ui-button-default ui-button-md">Créer le projet</button></div></div></div></div><pre class="catalog-example-snippet"><code>@ui.Modal(ui.ModalProps{
Title: &#34;Créer un projet&#34;, Title: &#34;Créer un projet&#34;,
Body: ui.FormField(...), Body: ui.FormField(...),
Actions: ui.Button(...), Actions: ui.Button(...),

View file

@ -237,7 +237,7 @@ func TestGetTablosPageUsesSharedToolbarButtonAndStatusBadge(t *testing.T) {
body := rec.Body.String() body := rec.Body.String()
for _, want := range []string{ for _, want := range []string{
`ui-button ui-button-primary ui-button-md`, `ui-button ui-button-solid ui-button-default ui-button-md`,
`ui-badge ui-badge-warning`, `ui-badge ui-badge-warning`,
} { } {
if !strings.Contains(body, want) { if !strings.Contains(body, want) {

View file

@ -3,6 +3,7 @@ package ui
type ButtonProps struct { type ButtonProps struct {
Label string Label string
Variant ButtonVariant Variant ButtonVariant
Tone ButtonTone
Size Size Size Size
Type string Type string
Icon string Icon string
@ -10,7 +11,7 @@ type ButtonProps struct {
} }
templ Button(props ButtonProps) { templ Button(props ButtonProps) {
<button type={ buttonType(props.Type) } class={ buttonClass(props.Variant, props.Size) } { props.Attrs... }> <button type={ buttonType(props.Type) } class={ buttonClass(props.Variant, props.Tone, props.Size) } { props.Attrs... }>
if props.Icon != "" { if props.Icon != "" {
<span class="ui-button-icon"> <span class="ui-button-icon">
@UIIcon(props.Icon) @UIIcon(props.Icon)

View file

@ -11,6 +11,7 @@ import templruntime "github.com/a-h/templ/runtime"
type ButtonProps struct { type ButtonProps struct {
Label string Label string
Variant ButtonVariant Variant ButtonVariant
Tone ButtonTone
Size Size Size Size
Type string Type string
Icon string Icon string
@ -38,7 +39,7 @@ func Button(props ButtonProps) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent templ_7745c5c3_Var1 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var2 = []any{buttonClass(props.Variant, props.Size)} var templ_7745c5c3_Var2 = []any{buttonClass(props.Variant, props.Tone, props.Size)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...) templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
@ -50,7 +51,7 @@ func Button(props ButtonProps) templ.Component {
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(buttonType(props.Type)) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(buttonType(props.Type))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/button.templ`, Line: 13, Col: 38} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/button.templ`, Line: 14, Col: 38}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -98,7 +99,7 @@ func Button(props ButtonProps) templ.Component {
var templ_7745c5c3_Var5 string var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(props.Label) templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(props.Label)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/button.templ`, Line: 19, Col: 15} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/button.templ`, Line: 20, Col: 15}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {

View file

@ -70,7 +70,8 @@ func TestButtonPageExamplesRenderRealPrimitives(t *testing.T) {
html := renderToString(t, page.Examples[0].Preview) html := renderToString(t, page.Examples[0].Preview)
for _, want := range []string{ for _, want := range []string{
`ui-button`, `ui-button`,
`ui-button-primary`, `ui-button-solid`,
`ui-button-default`,
`Nouveau projet`, `Nouveau projet`,
} { } {
if !strings.Contains(html, want) { if !strings.Contains(html, want) {

View file

@ -14,33 +14,53 @@ type anyComponent = templ.Component
func buttonExamples() []Example { func buttonExamples() []Example {
return []Example{ return []Example{
{ {
Title: "Primary action", Title: "Default solid action",
Description: "Used for the main action in a page section or modal footer.", Description: "Used for the main action in a page section or modal footer.",
Preview: ui.Button(ui.ButtonProps{ Preview: ui.Button(ui.ButtonProps{
Label: "Nouveau projet", Label: "Nouveau projet",
Variant: ui.ButtonVariantPrimary, Variant: ui.ButtonVariantDefault,
Size: ui.SizeMD, Size: ui.SizeMD,
Type: "button", Type: "button",
}), }),
Snippet: `@ui.Button(ui.ButtonProps{ Snippet: `@ui.Button(ui.ButtonProps{
Label: "Nouveau projet", Label: "Nouveau projet",
Variant: ui.ButtonVariantPrimary, Variant: ui.ButtonVariantDefault,
Size: ui.SizeMD, Size: ui.SizeMD,
Type: "button", Type: "button",
})`, })`,
}, },
{ {
Title: "Danger action", Title: "Soft warning action",
Description: "Used for inline actions that need emphasis without the weight of a solid button.",
Preview: ui.Button(ui.ButtonProps{
Label: "Relancer",
Variant: ui.ButtonVariantWarning,
Tone: ui.ButtonToneSoft,
Size: ui.SizeMD,
Type: "button",
}),
Snippet: `@ui.Button(ui.ButtonProps{
Label: "Relancer",
Variant: ui.ButtonVariantWarning,
Tone: ui.ButtonToneSoft,
Size: ui.SizeMD,
Type: "button",
})`,
},
{
Title: "Soft danger action",
Description: "Used for irreversible actions after explicit confirmation.", Description: "Used for irreversible actions after explicit confirmation.",
Preview: ui.Button(ui.ButtonProps{ Preview: ui.Button(ui.ButtonProps{
Label: "Supprimer", Label: "Supprimer",
Variant: ui.ButtonVariantDanger, Variant: ui.ButtonVariantDanger,
Tone: ui.ButtonToneSoft,
Size: ui.SizeLG, Size: ui.SizeLG,
Type: "submit", Type: "submit",
}), }),
Snippet: `@ui.Button(ui.ButtonProps{ Snippet: `@ui.Button(ui.ButtonProps{
Label: "Supprimer", Label: "Supprimer",
Variant: ui.ButtonVariantDanger, Variant: ui.ButtonVariantDanger,
Tone: ui.ButtonToneSoft,
Size: ui.SizeLG, Size: ui.SizeLG,
Type: "submit", Type: "submit",
})`, })`,
@ -205,13 +225,13 @@ func modalExamples() []Example {
return renderComponents(ctx, w, return renderComponents(ctx, w,
ui.Button(ui.ButtonProps{ ui.Button(ui.ButtonProps{
Label: "Annuler", Label: "Annuler",
Variant: ui.ButtonVariantSecondary, Variant: ui.ButtonVariantNeutral,
Size: ui.SizeMD, Size: ui.SizeMD,
Type: "button", Type: "button",
}), }),
ui.Button(ui.ButtonProps{ ui.Button(ui.ButtonProps{
Label: "Créer le projet", Label: "Créer le projet",
Variant: ui.ButtonVariantPrimary, Variant: ui.ButtonVariantDefault,
Size: ui.SizeMD, Size: ui.SizeMD,
Type: "submit", Type: "submit",
}), }),
@ -255,7 +275,7 @@ func emptyStateExamples() []Example {
Icon: ui.UIIcon("grid3x3"), Icon: ui.UIIcon("grid3x3"),
Action: ui.Button(ui.ButtonProps{ Action: ui.Button(ui.ButtonProps{
Label: "Nouveau projet", Label: "Nouveau projet",
Variant: ui.ButtonVariantPrimary, Variant: ui.ButtonVariantDefault,
Size: ui.SizeMD, Size: ui.SizeMD,
Type: "button", Type: "button",
Icon: "plus", Icon: "plus",

View file

@ -12,10 +12,10 @@ import (
"github.com/a-h/templ" "github.com/a-h/templ"
) )
func TestButtonRendersPrimaryMediumMarkup(t *testing.T) { func TestButtonRendersDefaultSolidMediumMarkup(t *testing.T) {
component := Button(ButtonProps{ component := Button(ButtonProps{
Label: "Nouveau projet", Label: "Nouveau projet",
Variant: ButtonVariantPrimary, Variant: ButtonVariantDefault,
Size: SizeMD, Size: SizeMD,
Type: "button", Type: "button",
}) })
@ -26,7 +26,8 @@ func TestButtonRendersPrimaryMediumMarkup(t *testing.T) {
`type="button"`, `type="button"`,
`Nouveau projet`, `Nouveau projet`,
`ui-button`, `ui-button`,
`ui-button-primary`, `ui-button-solid`,
`ui-button-default`,
`ui-button-md`, `ui-button-md`,
} { } {
if !strings.Contains(html, want) { if !strings.Contains(html, want) {
@ -101,7 +102,7 @@ func TestModalRendersShellStructure(t *testing.T) {
func TestButtonUsesSharedTokenBackedClasses(t *testing.T) { func TestButtonUsesSharedTokenBackedClasses(t *testing.T) {
component := Button(ButtonProps{ component := Button(ButtonProps{
Label: "Create", Label: "Create",
Variant: ButtonVariantPrimary, Variant: ButtonVariantDefault,
Size: SizeSM, Size: SizeSM,
Type: "button", Type: "button",
}) })
@ -110,7 +111,8 @@ func TestButtonUsesSharedTokenBackedClasses(t *testing.T) {
for _, want := range []string{ for _, want := range []string{
`ui-button`, `ui-button`,
`ui-button-primary`, `ui-button-solid`,
`ui-button-default`,
`ui-button-sm`, `ui-button-sm`,
} { } {
if !strings.Contains(html, want) { if !strings.Contains(html, want) {
@ -128,11 +130,12 @@ func TestSharedSemanticClassesExistInStylesheet(t *testing.T) {
css := string(body) css := string(body)
for _, want := range []string{ for _, want := range []string{
`.ui-button-primary`, `.ui-button-solid.ui-button-default`,
`.ui-button-sm`, `.ui-button-sm`,
`.ui-badge-warning`, `.ui-badge-warning`,
`.ui-modal-panel`, `.ui-modal-panel`,
`.borderless-icon-button`, `.borderless-icon-button`,
`.ui-button-soft.ui-button-danger`,
} { } {
if !strings.Contains(css, want) { if !strings.Contains(css, want) {
t.Fatalf("expected stylesheet to contain %q", want) t.Fatalf("expected stylesheet to contain %q", want)
@ -144,6 +147,7 @@ func TestButtonRendersDangerLargeMarkup(t *testing.T) {
component := Button(ButtonProps{ component := Button(ButtonProps{
Label: "Supprimer", Label: "Supprimer",
Variant: ButtonVariantDanger, Variant: ButtonVariantDanger,
Tone: ButtonToneSoft,
Size: SizeLG, Size: SizeLG,
Type: "submit", Type: "submit",
}) })
@ -152,6 +156,7 @@ func TestButtonRendersDangerLargeMarkup(t *testing.T) {
for _, want := range []string{ for _, want := range []string{
`type="submit"`, `type="submit"`,
`ui-button-soft`,
`ui-button-danger`, `ui-button-danger`,
`ui-button-lg`, `ui-button-lg`,
`Supprimer`, `Supprimer`,
@ -162,6 +167,30 @@ func TestButtonRendersDangerLargeMarkup(t *testing.T) {
} }
} }
func TestButtonRendersSoftWarningMarkup(t *testing.T) {
component := Button(ButtonProps{
Label: "Relancer",
Variant: ButtonVariantWarning,
Tone: ButtonToneSoft,
Size: SizeMD,
Type: "button",
})
html := renderToString(t, component)
for _, want := range []string{
`type="button"`,
`ui-button-soft`,
`ui-button-warning`,
`ui-button-md`,
`Relancer`,
} {
if !strings.Contains(html, want) {
t.Fatalf("expected %q in %q", want, html)
}
}
}
func TestInputRendersSharedControlMarkup(t *testing.T) { func TestInputRendersSharedControlMarkup(t *testing.T) {
component := Input(InputProps{ component := Input(InputProps{
Name: "name", Name: "name",

View file

@ -11,10 +11,18 @@ const (
type ButtonVariant string type ButtonVariant string
const ( const (
ButtonVariantPrimary ButtonVariant = "primary" ButtonVariantDefault ButtonVariant = "default"
ButtonVariantSecondary ButtonVariant = "secondary" ButtonVariantNeutral ButtonVariant = "neutral"
ButtonVariantGhost ButtonVariant = "ghost" ButtonVariantWarning ButtonVariant = "warning"
ButtonVariantDanger ButtonVariant = "danger" ButtonVariantSuccess ButtonVariant = "success"
ButtonVariantDanger ButtonVariant = "danger"
)
type ButtonTone string
const (
ButtonToneSolid ButtonTone = "solid"
ButtonToneSoft ButtonTone = "soft"
) )
type IconButtonVariant string type IconButtonVariant string
@ -33,8 +41,8 @@ const (
BadgeVariantDanger BadgeVariant = "danger" BadgeVariantDanger BadgeVariant = "danger"
) )
func buttonClass(variant ButtonVariant, size Size) string { func buttonClass(variant ButtonVariant, tone ButtonTone, size Size) string {
return "ui-button ui-button-" + string(normalizedButtonVariant(variant)) + " ui-button-" + string(normalizedSize(size)) return "ui-button ui-button-" + string(normalizedButtonTone(tone)) + " ui-button-" + string(normalizedButtonVariant(variant)) + " ui-button-" + string(normalizedSize(size))
} }
func iconButtonClass(variant IconButtonVariant) string { func iconButtonClass(variant IconButtonVariant) string {
@ -61,10 +69,19 @@ func normalizedSize(size Size) Size {
func normalizedButtonVariant(variant ButtonVariant) ButtonVariant { func normalizedButtonVariant(variant ButtonVariant) ButtonVariant {
switch variant { switch variant {
case ButtonVariantSecondary, ButtonVariantGhost, ButtonVariantDanger: case ButtonVariantNeutral, ButtonVariantWarning, ButtonVariantSuccess, ButtonVariantDanger:
return variant return variant
default: default:
return ButtonVariantPrimary return ButtonVariantDefault
}
}
func normalizedButtonTone(tone ButtonTone) ButtonTone {
switch tone {
case ButtonToneSoft:
return tone
default:
return ButtonToneSolid
} }
} }

View file

@ -8,7 +8,7 @@ templ TablosPageContent(vm TablosPageViewModel) {
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Mes Projets</h1> <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Mes Projets</h1>
@ui.Button(ui.ButtonProps{ @ui.Button(ui.ButtonProps{
Label: "Nouveau projet", Label: "Nouveau projet",
Variant: ui.ButtonVariantPrimary, Variant: ui.ButtonVariantDefault,
Size: ui.SizeMD, Size: ui.SizeMD,
Type: "button", Type: "button",
Icon: "plus", Icon: "plus",
@ -99,7 +99,7 @@ templ TablosPageContent(vm TablosPageViewModel) {
Icon: ui.UIIcon("grid3x3"), Icon: ui.UIIcon("grid3x3"),
Action: ui.Button(ui.ButtonProps{ Action: ui.Button(ui.ButtonProps{
Label: "Nouveau projet", Label: "Nouveau projet",
Variant: ui.ButtonVariantPrimary, Variant: ui.ButtonVariantDefault,
Size: ui.SizeMD, Size: ui.SizeMD,
Type: "button", Type: "button",
Icon: "plus", Icon: "plus",
@ -278,13 +278,13 @@ templ CreateTabloModalBody(vm TablosPageViewModel) {
hx-target="#app-main-content" hx-target="#app-main-content"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-push-url="true" hx-push-url="true"
class="ui-button ui-button-secondary ui-button-md" class="ui-button ui-button-solid ui-button-neutral ui-button-md"
> >
Annuler Annuler
</a> </a>
@ui.Button(ui.ButtonProps{ @ui.Button(ui.ButtonProps{
Label: "Créer le projet", Label: "Créer le projet",
Variant: ui.ButtonVariantPrimary, Variant: ui.ButtonVariantDefault,
Size: ui.SizeMD, Size: ui.SizeMD,
Type: "submit", Type: "submit",
}) })

View file

@ -37,7 +37,7 @@ func TablosPageContent(vm TablosPageViewModel) templ.Component {
} }
templ_7745c5c3_Err = ui.Button(ui.ButtonProps{ templ_7745c5c3_Err = ui.Button(ui.ButtonProps{
Label: "Nouveau projet", Label: "Nouveau projet",
Variant: ui.ButtonVariantPrimary, Variant: ui.ButtonVariantDefault,
Size: ui.SizeMD, Size: ui.SizeMD,
Type: "button", Type: "button",
Icon: "plus", Icon: "plus",
@ -287,7 +287,7 @@ func TablosPageContent(vm TablosPageViewModel) templ.Component {
Icon: ui.UIIcon("grid3x3"), Icon: ui.UIIcon("grid3x3"),
Action: ui.Button(ui.ButtonProps{ Action: ui.Button(ui.ButtonProps{
Label: "Nouveau projet", Label: "Nouveau projet",
Variant: ui.ButtonVariantPrimary, Variant: ui.ButtonVariantDefault,
Size: ui.SizeMD, Size: ui.SizeMD,
Type: "button", Type: "button",
Icon: "plus", Icon: "plus",
@ -1005,13 +1005,13 @@ func CreateTabloModalBody(vm TablosPageViewModel) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\" hx-target=\"#app-main-content\" hx-swap=\"outerHTML\" hx-push-url=\"true\" class=\"ui-button ui-button-secondary ui-button-md\">Annuler</a>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\" hx-target=\"#app-main-content\" hx-swap=\"outerHTML\" hx-push-url=\"true\" class=\"ui-button ui-button-solid ui-button-neutral ui-button-md\">Annuler</a>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = ui.Button(ui.ButtonProps{ templ_7745c5c3_Err = ui.Button(ui.ButtonProps{
Label: "Créer le projet", Label: "Créer le projet",
Variant: ui.ButtonVariantPrimary, Variant: ui.ButtonVariantDefault,
Size: ui.SizeMD, Size: ui.SizeMD,
Type: "submit", Type: "submit",
}).Render(ctx, templ_7745c5c3_Buffer) }).Render(ctx, templ_7745c5c3_Buffer)

View file

@ -1062,46 +1062,126 @@ input {
.ui-button-lg { .ui-button-lg {
font-size: 1rem; font-size: 1rem;
padding: 0.9rem 1.25rem; padding: 0.82rem 1.15rem;
} }
.ui-button-primary { .ui-button-solid.ui-button-default {
background: var(--secondary); background: var(--secondary);
color: #fff; color: #fff;
} }
.ui-button-primary:hover { .ui-button-solid.ui-button-default:hover {
background: #6d28d9; background: #6d28d9;
} }
.ui-button-secondary { .ui-button-solid.ui-button-default:active {
background: #5b21b6;
}
.ui-button-solid.ui-button-neutral {
background: #f3f4f6; background: #f3f4f6;
color: #111827; color: #111827;
} }
.ui-button-secondary:hover { .ui-button-solid.ui-button-neutral:hover {
background: #e5e7eb; background: #e5e7eb;
} }
.ui-button-ghost { .ui-button-solid.ui-button-neutral:active {
background: transparent; background: #d1d5db;
color: #4b5563;
} }
.ui-button-ghost:hover { .ui-button-solid.ui-button-warning {
background: #f9fafb; background: #db9729;
color: #111827; color: #fff;
} }
.ui-button-danger { .ui-button-solid.ui-button-warning:hover {
background: #c37f12;
}
.ui-button-solid.ui-button-warning:active {
background: #a9670c;
}
.ui-button-solid.ui-button-success {
background: #16a34a;
color: #fff;
}
.ui-button-solid.ui-button-success:hover {
background: #15803d;
}
.ui-button-solid.ui-button-success:active {
background: #166534;
}
.ui-button-solid.ui-button-danger {
background: #dc2626; background: #dc2626;
color: #fff; color: #fff;
} }
.ui-button-danger:hover { .ui-button-solid.ui-button-danger:hover {
background: #b91c1c; background: #b91c1c;
} }
.ui-button-solid.ui-button-danger:active {
background: #991b1b;
}
.ui-button-soft.ui-button-default {
background: #ede9fe;
color: #6d28d9;
}
.ui-button-soft.ui-button-default:hover {
background: #ddd6fe;
}
.ui-button-soft.ui-button-default:active {
background: #c4b5fd;
}
.ui-button-soft.ui-button-warning {
background: #fff4e2;
color: #b86e00;
}
.ui-button-soft.ui-button-warning:hover {
background: #fee6b7;
}
.ui-button-soft.ui-button-warning:active {
background: #fdd58e;
}
.ui-button-soft.ui-button-success {
background: #ecfdf3;
color: #15803d;
}
.ui-button-soft.ui-button-success:hover {
background: #d1fadf;
}
.ui-button-soft.ui-button-success:active {
background: #a6f4c5;
}
.ui-button-soft.ui-button-danger {
background: #fef3f2;
color: #b42318;
}
.ui-button-soft.ui-button-danger:hover {
background: #fee4e2;
}
.ui-button-soft.ui-button-danger:active {
background: #fecdca;
}
.ui-badge { .ui-badge {
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 999px; border-radius: 999px;