408 lines
10 KiB
Go
408 lines
10 KiB
Go
|
|
package catalog
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"io"
|
||
|
|
|
||
|
|
"github.com/a-h/templ"
|
||
|
|
|
||
|
|
"backend/internal/web/ui"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Example is a single rendered component variant with a templ call annotation.
|
||
|
|
type Example struct {
|
||
|
|
Title string
|
||
|
|
Preview templ.Component
|
||
|
|
Snippet string
|
||
|
|
}
|
||
|
|
|
||
|
|
type anyComponent = templ.Component
|
||
|
|
|
||
|
|
func badgeExamples() []Example {
|
||
|
|
return []Example{
|
||
|
|
{
|
||
|
|
Title: "Info",
|
||
|
|
Preview: ui.Badge(ui.BadgeProps{
|
||
|
|
Label: "Info",
|
||
|
|
Variant: ui.BadgeVariantInfo,
|
||
|
|
}),
|
||
|
|
Snippet: `@ui.Badge(ui.BadgeProps{Label: "Info", Variant: ui.BadgeVariantInfo})`,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
Title: "Warning",
|
||
|
|
Preview: ui.Badge(ui.BadgeProps{
|
||
|
|
Label: "Warning",
|
||
|
|
Variant: ui.BadgeVariantWarning,
|
||
|
|
}),
|
||
|
|
Snippet: `@ui.Badge(ui.BadgeProps{Label: "Warning", Variant: ui.BadgeVariantWarning})`,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
Title: "Success",
|
||
|
|
Preview: ui.Badge(ui.BadgeProps{
|
||
|
|
Label: "Success",
|
||
|
|
Variant: ui.BadgeVariantSuccess,
|
||
|
|
}),
|
||
|
|
Snippet: `@ui.Badge(ui.BadgeProps{Label: "Success", Variant: ui.BadgeVariantSuccess})`,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
Title: "Danger",
|
||
|
|
Preview: ui.Badge(ui.BadgeProps{
|
||
|
|
Label: "Danger",
|
||
|
|
Variant: ui.BadgeVariantDanger,
|
||
|
|
}),
|
||
|
|
Snippet: `@ui.Badge(ui.BadgeProps{Label: "Danger", Variant: ui.BadgeVariantDanger})`,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
Title: "Primary",
|
||
|
|
Preview: ui.Badge(ui.BadgeProps{
|
||
|
|
Label: "Primary",
|
||
|
|
Variant: ui.BadgeVariantPrimary,
|
||
|
|
}),
|
||
|
|
Snippet: `@ui.Badge(ui.BadgeProps{Label: "Primary", Variant: ui.BadgeVariantPrimary})`,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func buttonExamples() []Example {
|
||
|
|
return []Example{
|
||
|
|
{
|
||
|
|
Title: "Solid / Default",
|
||
|
|
Preview: ui.Button(ui.ButtonProps{
|
||
|
|
Label: "Create project",
|
||
|
|
Variant: ui.ButtonVariantDefault,
|
||
|
|
Tone: ui.ButtonToneSolid,
|
||
|
|
Size: ui.SizeMD,
|
||
|
|
Type: "button",
|
||
|
|
}),
|
||
|
|
Snippet: `@ui.Button(ui.ButtonProps{
|
||
|
|
Label: "Create project",
|
||
|
|
Variant: ui.ButtonVariantDefault,
|
||
|
|
Tone: ui.ButtonToneSolid,
|
||
|
|
Size: ui.SizeMD,
|
||
|
|
Type: "button",
|
||
|
|
})`,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
Title: "Solid / Danger",
|
||
|
|
Preview: ui.Button(ui.ButtonProps{
|
||
|
|
Label: "Delete",
|
||
|
|
Variant: ui.ButtonVariantDanger,
|
||
|
|
Tone: ui.ButtonToneSolid,
|
||
|
|
Size: ui.SizeMD,
|
||
|
|
Type: "button",
|
||
|
|
}),
|
||
|
|
Snippet: `@ui.Button(ui.ButtonProps{
|
||
|
|
Label: "Delete",
|
||
|
|
Variant: ui.ButtonVariantDanger,
|
||
|
|
Tone: ui.ButtonToneSolid,
|
||
|
|
Size: ui.SizeMD,
|
||
|
|
Type: "button",
|
||
|
|
})`,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
Title: "Soft / Neutral",
|
||
|
|
Preview: ui.Button(ui.ButtonProps{
|
||
|
|
Label: "Cancel",
|
||
|
|
Variant: ui.ButtonVariantNeutral,
|
||
|
|
Tone: ui.ButtonToneSoft,
|
||
|
|
Size: ui.SizeMD,
|
||
|
|
Type: "button",
|
||
|
|
}),
|
||
|
|
Snippet: `@ui.Button(ui.ButtonProps{
|
||
|
|
Label: "Cancel",
|
||
|
|
Variant: ui.ButtonVariantNeutral,
|
||
|
|
Tone: ui.ButtonToneSoft,
|
||
|
|
Size: ui.SizeMD,
|
||
|
|
Type: "button",
|
||
|
|
})`,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
Title: "Ghost",
|
||
|
|
Preview: ui.Button(ui.ButtonProps{
|
||
|
|
Label: "Learn more",
|
||
|
|
Variant: ui.ButtonVariantGhost,
|
||
|
|
Size: ui.SizeMD,
|
||
|
|
Type: "button",
|
||
|
|
}),
|
||
|
|
Snippet: `@ui.Button(ui.ButtonProps{
|
||
|
|
Label: "Learn more",
|
||
|
|
Variant: ui.ButtonVariantGhost,
|
||
|
|
Size: ui.SizeMD,
|
||
|
|
Type: "button",
|
||
|
|
})`,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func cardExamples() []Example {
|
||
|
|
return []Example{
|
||
|
|
{
|
||
|
|
Title: "Card with header, body, and footer",
|
||
|
|
Preview: ui.Card(ui.CardProps{
|
||
|
|
Header: textBody("Card Header"),
|
||
|
|
Body: textBody("Card body content goes here."),
|
||
|
|
Footer: textBody("Card Footer"),
|
||
|
|
}),
|
||
|
|
Snippet: `@ui.Card(ui.CardProps{
|
||
|
|
Header: headerComponent,
|
||
|
|
Body: bodyComponent,
|
||
|
|
Footer: footerComponent,
|
||
|
|
})`,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func emptyStateExamples() []Example {
|
||
|
|
return []Example{
|
||
|
|
{
|
||
|
|
Title: "Empty list",
|
||
|
|
Preview: ui.EmptyState(ui.EmptyStateProps{
|
||
|
|
Title: "Nothing here yet",
|
||
|
|
Description: "Add your first item to get started.",
|
||
|
|
Icon: ui.UIIcon("grid3x3"),
|
||
|
|
Action: ui.Button(ui.ButtonProps{
|
||
|
|
Label: "Add item",
|
||
|
|
Variant: ui.ButtonVariantDefault,
|
||
|
|
Tone: ui.ButtonToneSolid,
|
||
|
|
Size: ui.SizeMD,
|
||
|
|
Type: "button",
|
||
|
|
Icon: "plus",
|
||
|
|
}),
|
||
|
|
}),
|
||
|
|
Snippet: `@ui.EmptyState(ui.EmptyStateProps{
|
||
|
|
Title: "Nothing here yet",
|
||
|
|
Description: "Add your first item to get started.",
|
||
|
|
Icon: ui.UIIcon("grid3x3"),
|
||
|
|
Action: ui.Button(...),
|
||
|
|
})`,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func formFieldExamples() []Example {
|
||
|
|
return []Example{
|
||
|
|
{
|
||
|
|
Title: "Field with label, hint, and error",
|
||
|
|
Preview: ui.FormField(ui.FormFieldProps{
|
||
|
|
Label: "Project name",
|
||
|
|
For: "catalog-name",
|
||
|
|
Field: ui.Input(ui.InputProps{
|
||
|
|
ID: "catalog-name",
|
||
|
|
Name: "name",
|
||
|
|
Placeholder: "Enter a name",
|
||
|
|
Type: "text",
|
||
|
|
}),
|
||
|
|
Hint: "Enter a value between 1 and 100.",
|
||
|
|
Error: "This field is required.",
|
||
|
|
}),
|
||
|
|
Snippet: `@ui.FormField(ui.FormFieldProps{
|
||
|
|
Label: "Project name",
|
||
|
|
For: "catalog-name",
|
||
|
|
Field: ui.Input(ui.InputProps{...}),
|
||
|
|
Hint: "Enter a value between 1 and 100.",
|
||
|
|
Error: "This field is required.",
|
||
|
|
})`,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func iconButtonExamples() []Example {
|
||
|
|
return []Example{
|
||
|
|
{
|
||
|
|
Title: "Ghost / Neutral (plus icon)",
|
||
|
|
Preview: ui.IconButton(ui.IconButtonProps{
|
||
|
|
Label: "Add item",
|
||
|
|
Icon: "plus",
|
||
|
|
Variant: ui.IconButtonVariantNeutral,
|
||
|
|
Tone: ui.IconButtonToneGhost,
|
||
|
|
Type: "button",
|
||
|
|
}),
|
||
|
|
Snippet: `@ui.IconButton(ui.IconButtonProps{
|
||
|
|
Label: "Add item",
|
||
|
|
Icon: "plus",
|
||
|
|
Variant: ui.IconButtonVariantNeutral,
|
||
|
|
Tone: ui.IconButtonToneGhost,
|
||
|
|
Type: "button",
|
||
|
|
})`,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
Title: "Solid / Danger (trash icon)",
|
||
|
|
Preview: ui.IconButton(ui.IconButtonProps{
|
||
|
|
Label: "Delete item",
|
||
|
|
Icon: "trash",
|
||
|
|
Variant: ui.IconButtonVariantDanger,
|
||
|
|
Tone: ui.IconButtonToneSolid,
|
||
|
|
Type: "button",
|
||
|
|
}),
|
||
|
|
Snippet: `@ui.IconButton(ui.IconButtonProps{
|
||
|
|
Label: "Delete item",
|
||
|
|
Icon: "trash",
|
||
|
|
Variant: ui.IconButtonVariantDanger,
|
||
|
|
Tone: ui.IconButtonToneSolid,
|
||
|
|
Type: "button",
|
||
|
|
})`,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func inputExamples() []Example {
|
||
|
|
return []Example{
|
||
|
|
{
|
||
|
|
Title: "Text input",
|
||
|
|
Preview: ui.Input(ui.InputProps{
|
||
|
|
Name: "name",
|
||
|
|
Placeholder: "Enter text here",
|
||
|
|
Type: "text",
|
||
|
|
}),
|
||
|
|
Snippet: `@ui.Input(ui.InputProps{
|
||
|
|
Name: "name",
|
||
|
|
Placeholder: "Enter text here",
|
||
|
|
Type: "text",
|
||
|
|
})`,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
Title: "Email input",
|
||
|
|
Preview: ui.Input(ui.InputProps{
|
||
|
|
Name: "email",
|
||
|
|
Placeholder: "you@example.com",
|
||
|
|
Type: "email",
|
||
|
|
}),
|
||
|
|
Snippet: `@ui.Input(ui.InputProps{
|
||
|
|
Name: "email",
|
||
|
|
Placeholder: "you@example.com",
|
||
|
|
Type: "email",
|
||
|
|
})`,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func modalExamples() []Example {
|
||
|
|
return []Example{
|
||
|
|
{
|
||
|
|
Title: "Modal panel (no backdrop)",
|
||
|
|
// Render only the panel div — not the full Modal component with backdrop.
|
||
|
|
// Pitfall 7: ui-modal-backdrop is position:fixed and would overlay the catalog page.
|
||
|
|
Preview: componentFunc(func(ctx context.Context, w io.Writer) error {
|
||
|
|
_, err := io.WriteString(w, `<div class="ui-modal-panel" style="position:relative;max-width:32rem;margin:0 auto;">`)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if err := renderComponents(ctx, w,
|
||
|
|
textBody(`<div class="ui-modal-header"><h2>Confirm action</h2></div>`),
|
||
|
|
textBody(`<div class="ui-modal-body"><p>Are you sure you want to proceed?</p></div>`),
|
||
|
|
textBody(`<div class="ui-modal-actions">`),
|
||
|
|
); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if err := ui.Button(ui.ButtonProps{
|
||
|
|
Label: "Cancel",
|
||
|
|
Variant: ui.ButtonVariantNeutral,
|
||
|
|
Tone: ui.ButtonToneSoft,
|
||
|
|
Size: ui.SizeMD,
|
||
|
|
Type: "button",
|
||
|
|
}).Render(ctx, w); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if err := ui.Button(ui.ButtonProps{
|
||
|
|
Label: "Confirm",
|
||
|
|
Variant: ui.ButtonVariantDefault,
|
||
|
|
Tone: ui.ButtonToneSolid,
|
||
|
|
Size: ui.SizeMD,
|
||
|
|
Type: "button",
|
||
|
|
}).Render(ctx, w); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
_, err = io.WriteString(w, `</div></div>`)
|
||
|
|
return err
|
||
|
|
}),
|
||
|
|
Snippet: `@ui.Modal(ui.ModalProps{
|
||
|
|
Title: "Confirm action",
|
||
|
|
Body: bodyContent,
|
||
|
|
Actions: actionButtons,
|
||
|
|
})`,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func selectExamples() []Example {
|
||
|
|
return []Example{
|
||
|
|
{
|
||
|
|
Title: "Single select",
|
||
|
|
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"},
|
||
|
|
},
|
||
|
|
})`,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func tableExamples() []Example {
|
||
|
|
return []Example{
|
||
|
|
{
|
||
|
|
Title: "Data table",
|
||
|
|
Preview: ui.Table(ui.TableProps{
|
||
|
|
Head: textBody(`<tr><th style="padding:0.75rem 1rem;text-align:left;font-size:0.75rem;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;">Name</th><th style="padding:0.75rem 1rem;text-align:left;font-size:0.75rem;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;">Status</th></tr>`),
|
||
|
|
Body: textBody(`<tr><td style="padding:0.75rem 1rem;">Example Project</td><td style="padding:0.75rem 1rem;">In progress</td></tr><tr><td style="padding:0.75rem 1rem;">Another Project</td><td style="padding:0.75rem 1rem;">Done</td></tr>`),
|
||
|
|
}),
|
||
|
|
Snippet: `@ui.Table(ui.TableProps{
|
||
|
|
Head: tableHead,
|
||
|
|
Body: tableBody,
|
||
|
|
})`,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func textareaExamples() []Example {
|
||
|
|
return []Example{
|
||
|
|
{
|
||
|
|
Title: "Textarea with placeholder",
|
||
|
|
Preview: ui.Textarea(ui.TextareaProps{
|
||
|
|
Name: "description",
|
||
|
|
Placeholder: "Enter a description...",
|
||
|
|
Rows: 4,
|
||
|
|
}),
|
||
|
|
Snippet: `@ui.Textarea(ui.TextareaProps{
|
||
|
|
Name: "description",
|
||
|
|
Placeholder: "Enter a description...",
|
||
|
|
Rows: 4,
|
||
|
|
})`,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func componentFunc(fn func(context.Context, io.Writer) error) templ.Component {
|
||
|
|
return templ.ComponentFunc(fn)
|
||
|
|
}
|
||
|
|
|
||
|
|
func textBody(text string) templ.Component {
|
||
|
|
return templ.ComponentFunc(func(_ context.Context, w io.Writer) error {
|
||
|
|
_, err := io.WriteString(w, text)
|
||
|
|
return err
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func renderComponents(ctx context.Context, w io.Writer, components ...templ.Component) error {
|
||
|
|
for _, c := range components {
|
||
|
|
if err := c.Render(ctx, w); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|