482 lines
13 KiB
Go
482 lines
13 KiB
Go
package catalog
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
|
|
"github.com/a-h/templ"
|
|
|
|
"xtablo-backend/internal/web/ui"
|
|
)
|
|
|
|
type anyComponent = templ.Component
|
|
|
|
func buttonExamples() []Example {
|
|
return []Example{
|
|
{
|
|
Title: "Default solid action",
|
|
Description: "Used for the main action in a page section or modal footer.",
|
|
Preview: ui.Button(ui.ButtonProps{
|
|
Label: "Nouveau projet",
|
|
Variant: ui.ButtonVariantDefault,
|
|
Size: ui.SizeMD,
|
|
Type: "button",
|
|
}),
|
|
Snippet: `@ui.Button(ui.ButtonProps{
|
|
Label: "Nouveau projet",
|
|
Variant: ui.ButtonVariantDefault,
|
|
Size: ui.SizeMD,
|
|
Type: "button",
|
|
})`,
|
|
},
|
|
{
|
|
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.",
|
|
Preview: ui.Button(ui.ButtonProps{
|
|
Label: "Supprimer",
|
|
Variant: ui.ButtonVariantDanger,
|
|
Tone: ui.ButtonToneSoft,
|
|
Size: ui.SizeLG,
|
|
Type: "submit",
|
|
}),
|
|
Snippet: `@ui.Button(ui.ButtonProps{
|
|
Label: "Supprimer",
|
|
Variant: ui.ButtonVariantDanger,
|
|
Tone: ui.ButtonToneSoft,
|
|
Size: ui.SizeLG,
|
|
Type: "submit",
|
|
})`,
|
|
},
|
|
}
|
|
}
|
|
|
|
func tokenExamples() []Example {
|
|
return []Example{
|
|
{
|
|
Title: "Status tones",
|
|
Description: "Shared semantic badges for info, warning, success, and danger states.",
|
|
Preview: componentFunc(func(ctx context.Context, w io.Writer) error {
|
|
for _, component := range []templ.Component{
|
|
ui.Badge(ui.BadgeProps{Label: "À faire", Variant: ui.BadgeVariantInfo}),
|
|
ui.Badge(ui.BadgeProps{Label: "En cours", Variant: ui.BadgeVariantWarning}),
|
|
ui.Badge(ui.BadgeProps{Label: "Terminé", Variant: ui.BadgeVariantSuccess}),
|
|
ui.Badge(ui.BadgeProps{Label: "Erreur", Variant: ui.BadgeVariantDanger}),
|
|
} {
|
|
if _, err := io.WriteString(w, `<div class="catalog-inline">`); err != nil {
|
|
return err
|
|
}
|
|
if err := component.Render(ctx, w); err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.WriteString(w, `</div>`); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}),
|
|
Snippet: `@ui.Badge(ui.BadgeProps{Label: "En cours", Variant: ui.BadgeVariantWarning})`,
|
|
},
|
|
}
|
|
}
|
|
|
|
func badgeExamples() []Example {
|
|
return []Example{
|
|
{
|
|
Title: "Status set",
|
|
Description: "The four semantic badge tones used across the app.",
|
|
Preview: componentFunc(func(ctx context.Context, w io.Writer) error {
|
|
return renderInlineComponents(ctx, w,
|
|
ui.Badge(ui.BadgeProps{Label: "À faire", Variant: ui.BadgeVariantInfo}),
|
|
ui.Badge(ui.BadgeProps{Label: "En cours", Variant: ui.BadgeVariantWarning}),
|
|
ui.Badge(ui.BadgeProps{Label: "Terminé", Variant: ui.BadgeVariantSuccess}),
|
|
ui.Badge(ui.BadgeProps{Label: "Erreur", Variant: ui.BadgeVariantDanger}),
|
|
)
|
|
}),
|
|
Snippet: `@ui.Badge(ui.BadgeProps{Label: "En cours", Variant: ui.BadgeVariantWarning})`,
|
|
},
|
|
}
|
|
}
|
|
|
|
func iconButtonExamples() []Example {
|
|
return []Example{
|
|
{
|
|
Title: "Borderless destructive action",
|
|
Description: "Used for delete controls inside project cards and list rows.",
|
|
Preview: ui.IconButton(ui.IconButtonProps{
|
|
Label: "Supprimer le projet",
|
|
Icon: "trash",
|
|
Variant: ui.IconButtonVariantDanger,
|
|
Tone: ui.IconButtonToneGhost,
|
|
Type: "button",
|
|
}),
|
|
Snippet: `@ui.IconButton(ui.IconButtonProps{
|
|
Label: "Supprimer le projet",
|
|
Icon: "trash",
|
|
Variant: ui.IconButtonVariantDanger,
|
|
Tone: ui.IconButtonToneGhost,
|
|
Type: "button",
|
|
})`,
|
|
},
|
|
{
|
|
Title: "Borderless neutral action",
|
|
Description: "Used for lightweight edit or details actions inside cards and list rows.",
|
|
Preview: ui.IconButton(ui.IconButtonProps{
|
|
Label: "Modifier le projet",
|
|
Icon: "pencil",
|
|
Variant: ui.IconButtonVariantNeutral,
|
|
Tone: ui.IconButtonToneGhost,
|
|
Type: "button",
|
|
}),
|
|
Snippet: `@ui.IconButton(ui.IconButtonProps{
|
|
Label: "Modifier le projet",
|
|
Icon: "pencil",
|
|
Variant: ui.IconButtonVariantNeutral,
|
|
Tone: ui.IconButtonToneGhost,
|
|
Type: "button",
|
|
})`,
|
|
},
|
|
}
|
|
}
|
|
|
|
func inputExamples() []Example {
|
|
return []Example{
|
|
{
|
|
Title: "Text input",
|
|
Description: "Single-line input for names, titles, and short labels.",
|
|
Preview: ui.Input(ui.InputProps{
|
|
Name: "name",
|
|
Value: "Projet Atlas",
|
|
Placeholder: "Nom du projet",
|
|
Type: "text",
|
|
}),
|
|
Snippet: `@ui.Input(ui.InputProps{
|
|
Name: "name",
|
|
Value: "Projet Atlas",
|
|
Placeholder: "Nom du projet",
|
|
Type: "text",
|
|
})`,
|
|
},
|
|
{
|
|
Title: "Textarea",
|
|
Description: "Multiline field for longer project notes and descriptions.",
|
|
Preview: ui.Textarea(ui.TextareaProps{
|
|
Name: "description",
|
|
Value: "Une description de projet plus détaillée.",
|
|
Placeholder: "Description",
|
|
Rows: 4,
|
|
}),
|
|
Snippet: `@ui.Textarea(ui.TextareaProps{
|
|
Name: "description",
|
|
Value: "Une description de projet plus détaillée.",
|
|
Placeholder: "Description",
|
|
Rows: 4,
|
|
})`,
|
|
},
|
|
}
|
|
}
|
|
|
|
func selectExamples() []Example {
|
|
return []Example{
|
|
{
|
|
Title: "Single select",
|
|
Description: "Single-choice dropdown with the shared input shell and custom chevron.",
|
|
Preview: ui.Select(ui.SelectProps{
|
|
Name: "status",
|
|
Placeholder: "Select a status",
|
|
Value: "in-progress",
|
|
Options: []ui.SelectOption{
|
|
{Value: "todo", Label: "To do"},
|
|
{Value: "in-progress", Label: "In progress"},
|
|
{Value: "done", Label: "Done"},
|
|
},
|
|
}),
|
|
Snippet: `@ui.Select(ui.SelectProps{
|
|
Name: "status",
|
|
Placeholder: "Select a status",
|
|
Value: "in-progress",
|
|
Options: []ui.SelectOption{
|
|
{Value: "todo", Label: "To do"},
|
|
{Value: "in-progress", Label: "In progress"},
|
|
{Value: "done", Label: "Done"},
|
|
},
|
|
})`,
|
|
},
|
|
{
|
|
Title: "Multiple select",
|
|
Description: "Multi-value selection with inline pills that stay form-compatible.",
|
|
Preview: ui.Select(ui.SelectProps{
|
|
Name: "assignee_ids",
|
|
Placeholder: "Select multiple values",
|
|
Multiple: true,
|
|
Values: []string{"alice", "bob"},
|
|
Options: []ui.SelectOption{
|
|
{Value: "alice", Label: "Alice"},
|
|
{Value: "bob", Label: "Bob"},
|
|
{Value: "charlie", Label: "Charlie"},
|
|
},
|
|
}),
|
|
Snippet: `@ui.Select(ui.SelectProps{
|
|
Name: "assignee_ids",
|
|
Placeholder: "Select multiple values",
|
|
Multiple: true,
|
|
Values: []string{"alice", "bob"},
|
|
Options: []ui.SelectOption{
|
|
{Value: "alice", Label: "Alice"},
|
|
{Value: "bob", Label: "Bob"},
|
|
{Value: "charlie", Label: "Charlie"},
|
|
},
|
|
})`,
|
|
},
|
|
}
|
|
}
|
|
|
|
func formFieldExamples() []Example {
|
|
return []Example{
|
|
{
|
|
Title: "Field with validation",
|
|
Description: "Wraps a control with label and inline error feedback.",
|
|
Preview: ui.FormField(ui.FormFieldProps{
|
|
Label: "Nom",
|
|
For: "catalog-name",
|
|
Field: ui.Input(ui.InputProps{
|
|
ID: "catalog-name",
|
|
Name: "name",
|
|
Placeholder: "Nom du projet",
|
|
Type: "text",
|
|
}),
|
|
Error: "Le nom est requis",
|
|
}),
|
|
Snippet: `@ui.FormField(ui.FormFieldProps{
|
|
Label: "Nom",
|
|
For: "catalog-name",
|
|
Field: ui.Input(ui.InputProps{
|
|
ID: "catalog-name",
|
|
Name: "name",
|
|
Placeholder: "Nom du projet",
|
|
Type: "text",
|
|
}),
|
|
Error: "Le nom est requis",
|
|
})`,
|
|
},
|
|
}
|
|
}
|
|
|
|
func modalExamples() []Example {
|
|
return []Example{
|
|
{
|
|
Title: "Create modal",
|
|
Description: "Shared modal shell with a form body and action footer.",
|
|
Preview: ui.Modal(ui.ModalProps{
|
|
Title: "Créer un projet",
|
|
Body: ui.FormField(ui.FormFieldProps{
|
|
Label: "Nom du projet",
|
|
For: "modal-name",
|
|
Field: ui.Input(ui.InputProps{
|
|
ID: "modal-name",
|
|
Name: "name",
|
|
Placeholder: "Nom du projet",
|
|
Type: "text",
|
|
}),
|
|
}),
|
|
Actions: componentFunc(func(ctx context.Context, w io.Writer) error {
|
|
return renderComponents(ctx, w,
|
|
ui.Button(ui.ButtonProps{
|
|
Label: "Annuler",
|
|
Variant: ui.ButtonVariantNeutral,
|
|
Size: ui.SizeMD,
|
|
Type: "button",
|
|
}),
|
|
ui.Button(ui.ButtonProps{
|
|
Label: "Créer le projet",
|
|
Variant: ui.ButtonVariantDefault,
|
|
Size: ui.SizeMD,
|
|
Type: "submit",
|
|
}),
|
|
)
|
|
}),
|
|
}),
|
|
Snippet: `@ui.Modal(ui.ModalProps{
|
|
Title: "Créer un projet",
|
|
Body: ui.FormField(...),
|
|
Actions: ui.Button(...),
|
|
})`,
|
|
},
|
|
}
|
|
}
|
|
|
|
func spacingExamples() []Example {
|
|
return []Example{
|
|
{
|
|
Title: "Horizontal spacing",
|
|
Description: "Use SpaceX to insert fixed horizontal gaps between inline or row-aligned components.",
|
|
Preview: componentFunc(func(ctx context.Context, w io.Writer) error {
|
|
if _, err := io.WriteString(w, `<div class="catalog-spacing-row">`); err != nil {
|
|
return err
|
|
}
|
|
for _, component := range []templ.Component{
|
|
ui.Button(ui.ButtonProps{
|
|
Label: "Précédent",
|
|
Variant: ui.ButtonVariantNeutral,
|
|
Size: ui.SizeMD,
|
|
Type: "button",
|
|
}),
|
|
ui.SpaceX(ui.SpaceProps{Size: ui.SpacingStepLG}),
|
|
ui.Button(ui.ButtonProps{
|
|
Label: "Suivant",
|
|
Variant: ui.ButtonVariantDefault,
|
|
Size: ui.SizeMD,
|
|
Type: "button",
|
|
}),
|
|
} {
|
|
if err := component.Render(ctx, w); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
_, err := io.WriteString(w, `</div>`)
|
|
return err
|
|
}),
|
|
Snippet: `@ui.SpaceX(ui.SpaceProps{Size: ui.SpacingStepLG})`,
|
|
},
|
|
{
|
|
Title: "Vertical spacing",
|
|
Description: "Use SpaceY to insert fixed vertical gaps between stacked blocks.",
|
|
Preview: componentFunc(func(ctx context.Context, w io.Writer) error {
|
|
if _, err := io.WriteString(w, `<div class="catalog-spacing-column">`); err != nil {
|
|
return err
|
|
}
|
|
for _, component := range []templ.Component{
|
|
ui.Card(ui.CardProps{
|
|
Body: textComponent("Bloc 1"),
|
|
}),
|
|
ui.SpaceY(ui.SpaceProps{Size: ui.SpacingStepMD}),
|
|
ui.Card(ui.CardProps{
|
|
Body: textComponent("Bloc 2"),
|
|
}),
|
|
} {
|
|
if err := component.Render(ctx, w); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
_, err := io.WriteString(w, `</div>`)
|
|
return err
|
|
}),
|
|
Snippet: `@ui.SpaceY(ui.SpaceProps{Size: ui.SpacingStepMD})`,
|
|
},
|
|
}
|
|
}
|
|
|
|
func tableExamples() []Example {
|
|
return []Example{
|
|
{
|
|
Title: "List shell",
|
|
Description: "Shared wrapper for server-rendered resource tables.",
|
|
Preview: ui.Table(ui.TableProps{
|
|
Head: textComponent(`<tr><th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Projet</th><th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Statut</th></tr>`),
|
|
Body: textComponent(`<tr><td class="px-6 py-4">Table View</td><td class="px-6 py-4">En cours</td></tr>`),
|
|
}),
|
|
Snippet: `@ui.Table(ui.TableProps{
|
|
Head: TabloListHead(),
|
|
Body: TabloListBody(tablos),
|
|
})`,
|
|
},
|
|
}
|
|
}
|
|
|
|
func emptyStateExamples() []Example {
|
|
return []Example{
|
|
{
|
|
Title: "Centered empty state",
|
|
Description: "Used when a list has no rows yet and the next action should stay obvious.",
|
|
Preview: ui.EmptyState(ui.EmptyStateProps{
|
|
Title: "Aucun projet trouvé",
|
|
Description: "Créez votre premier projet",
|
|
Icon: ui.UIIcon("grid3x3"),
|
|
Action: ui.Button(ui.ButtonProps{
|
|
Label: "Nouveau projet",
|
|
Variant: ui.ButtonVariantDefault,
|
|
Size: ui.SizeMD,
|
|
Type: "button",
|
|
Icon: "plus",
|
|
}),
|
|
}),
|
|
Snippet: `@ui.EmptyState(ui.EmptyStateProps{
|
|
Title: "Aucun projet trouvé",
|
|
Description: "Créez votre premier projet",
|
|
Icon: ui.UIIcon("grid3x3"),
|
|
Action: ui.Button(...),
|
|
})`,
|
|
},
|
|
}
|
|
}
|
|
|
|
func cardExamples() []Example {
|
|
return []Example{
|
|
{
|
|
Title: "Surface card",
|
|
Description: "Generic elevated surface with optional header and footer.",
|
|
Preview: ui.Card(ui.CardProps{
|
|
Header: textComponent("Header"),
|
|
Body: textComponent("Body"),
|
|
Footer: textComponent("Footer"),
|
|
}),
|
|
Snippet: `@ui.Card(ui.CardProps{
|
|
Header: textComponent("Header"),
|
|
Body: textComponent("Body"),
|
|
Footer: textComponent("Footer"),
|
|
})`,
|
|
},
|
|
}
|
|
}
|
|
|
|
func componentFunc(fn func(context.Context, io.Writer) error) templ.Component {
|
|
return templ.ComponentFunc(fn)
|
|
}
|
|
|
|
func textComponent(text string) templ.Component {
|
|
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
|
|
_, err := io.WriteString(w, text)
|
|
return err
|
|
})
|
|
}
|
|
|
|
func renderInlineComponents(ctx context.Context, w io.Writer, components ...templ.Component) error {
|
|
for _, component := range components {
|
|
if _, err := io.WriteString(w, `<div class="catalog-inline">`); err != nil {
|
|
return err
|
|
}
|
|
if err := component.Render(ctx, w); err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.WriteString(w, `</div>`); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func renderComponents(ctx context.Context, w io.Writer, components ...templ.Component) error {
|
|
for _, component := range components {
|
|
if err := component.Render(ctx, w); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|