feat(13-05): create catalog package with route files and router wiring
- Add backend/internal/web/ui/catalog/catalog.templ: single-page layout with 240px sidebar nav (11 anchor links) and 11 component sections with section headings matching DS-XX requirement IDs - Add backend/internal/web/ui/catalog/examples.go: Example struct + typed example functions for all 11 component types (badge/button/card/empty-state/ form-field/icon-button/input/modal/select/table/textarea); modal renders panel-only (no backdrop wrapper, Pitfall 7) - Add backend/internal/web/catalog_route_catalog.go (//go:build catalog): RegisterCatalogRoute mounts GET /ui-catalog via catalogPageHandler() - Add backend/internal/web/catalog_route_stub.go (//go:build !catalog): no-op RegisterCatalogRoute for production builds - Wire RegisterCatalogRoute(r) unconditionally in NewRouter after protected routes - Add justfile catalog target: just generate + go run -tags catalog ./cmd/web - go build ./... and go build -tags catalog ./... both pass; go test ./... green
This commit is contained in:
parent
f57952b6fb
commit
4046783fd4
6 changed files with 544 additions and 0 deletions
24
backend/internal/web/catalog_route_catalog.go
Normal file
24
backend/internal/web/catalog_route_catalog.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
//go:build catalog
|
||||
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"backend/internal/web/ui/catalog"
|
||||
)
|
||||
|
||||
// RegisterCatalogRoute mounts the /ui-catalog route.
|
||||
// This file is only compiled with -tags catalog (dev tool; never ships to production).
|
||||
func RegisterCatalogRoute(r chi.Router) {
|
||||
r.Get("/ui-catalog", catalogPageHandler())
|
||||
}
|
||||
|
||||
func catalogPageHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
templ.Handler(catalog.CatalogPage()).ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
9
backend/internal/web/catalog_route_stub.go
Normal file
9
backend/internal/web/catalog_route_stub.go
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
//go:build !catalog
|
||||
|
||||
package web
|
||||
|
||||
import "github.com/go-chi/chi/v5"
|
||||
|
||||
// RegisterCatalogRoute is a no-op in production builds.
|
||||
// The catalog route is only available when built with -tags catalog.
|
||||
func RegisterCatalogRoute(r chi.Router) {} //nolint:revive
|
||||
|
|
@ -146,6 +146,10 @@ func NewRouter(pinger Pinger, staticFS fs.FS, deps AuthDeps, tabloDeps TablosDep
|
|||
r.Post("/tablos/{id}/files/{file_id}/delete", FileDeleteHandler(fileDeps))
|
||||
})
|
||||
|
||||
// Dev-only catalog route (build tag: -tags catalog).
|
||||
// In production builds catalog_route_stub.go provides the no-op symbol.
|
||||
RegisterCatalogRoute(r)
|
||||
|
||||
// Liveness probe (D-12): always 200, no DB contact.
|
||||
r.Get("/healthz", HealthzHandler())
|
||||
// Readiness probe (D-13): probes DB; 200 when ready, 503 when degraded.
|
||||
|
|
|
|||
87
backend/internal/web/ui/catalog/catalog.templ
Normal file
87
backend/internal/web/ui/catalog/catalog.templ
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
package catalog
|
||||
|
||||
// CatalogPage renders the single-page component catalog.
|
||||
// Layout: fixed 240px sidebar + fluid main content area.
|
||||
// The catalog uses Tailwind utility classes for its shell — not app.css (D-A04).
|
||||
// Built with -tags catalog only; never compiled into production binaries.
|
||||
templ CatalogPage() {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Component Catalog</title>
|
||||
<style>
|
||||
/* Catalog shell — minimal reset only, no app.css import (D-A04) */
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f9fafb; color: #111827; }
|
||||
.catalog-layout { display: flex; min-height: 100vh; }
|
||||
.catalog-sidebar { width: 240px; flex-shrink: 0; background: #fff; border-right: 1px solid #e5e7eb; position: sticky; top: 0; height: 100vh; overflow-y: auto; padding: 1.5rem 1rem; }
|
||||
.catalog-sidebar-heading { font-size: 0.75rem; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: #6b7280; margin: 0 0 0.75rem 0.5rem; }
|
||||
.catalog-nav-link { display: block; padding: 0.4rem 0.5rem; border-radius: 0.375rem; text-decoration: none; color: #374151; font-size: 0.875rem; }
|
||||
.catalog-nav-link:hover { background: #f3f4f6; }
|
||||
.catalog-main { flex: 1; padding: 2rem 3rem; max-width: 960px; }
|
||||
.catalog-title { font-size: 1.875rem; font-weight: 700; margin: 0 0 0.25rem 0; color: #111827; }
|
||||
.catalog-subtitle { color: #6b7280; margin: 0 0 2.5rem 0; }
|
||||
.catalog-section { margin-bottom: 3.5rem; }
|
||||
.catalog-section-heading { font-size: 1.125rem; font-weight: 600; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.5rem; margin: 0 0 1.5rem 0; color: #111827; }
|
||||
.catalog-example { margin-bottom: 2rem; }
|
||||
.catalog-example-title { font-size: 0.875rem; font-weight: 600; color: #374151; margin: 0 0 0.5rem 0; }
|
||||
.catalog-example-preview { padding: 1.25rem; background: #fff; border: 1px solid #e5e7eb; border-radius: 0.5rem; margin-bottom: 0.5rem; }
|
||||
.catalog-example-snippet { background: #f3f4f6; border: 1px solid #e5e7eb; border-radius: 0.375rem; padding: 0.75rem 1rem; font-size: 0.75rem; font-family: ui-monospace, "Cascadia Code", "Source Code Pro", monospace; overflow-x: auto; white-space: pre; margin: 0; color: #374151; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="catalog-layout">
|
||||
<aside class="catalog-sidebar">
|
||||
<p class="catalog-sidebar-heading">Components</p>
|
||||
<nav>
|
||||
<a href="#badge" class="catalog-nav-link">Badge</a>
|
||||
<a href="#button" class="catalog-nav-link">Button</a>
|
||||
<a href="#card" class="catalog-nav-link">Card</a>
|
||||
<a href="#empty-state" class="catalog-nav-link">Empty State</a>
|
||||
<a href="#form-field" class="catalog-nav-link">Form Field</a>
|
||||
<a href="#icon-button" class="catalog-nav-link">Icon Button</a>
|
||||
<a href="#input" class="catalog-nav-link">Input</a>
|
||||
<a href="#modal" class="catalog-nav-link">Modal</a>
|
||||
<a href="#select" class="catalog-nav-link">Select</a>
|
||||
<a href="#table" class="catalog-nav-link">Table</a>
|
||||
<a href="#textarea" class="catalog-nav-link">Textarea</a>
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="catalog-main">
|
||||
<h1 class="catalog-title">Component Catalog</h1>
|
||||
<p class="catalog-subtitle">All 11 component types with variants. Visual sign-off gate for Phase 14.</p>
|
||||
@catalogSection("badge", "Badge — DS-05", badgeExamples())
|
||||
@catalogSection("button", "Button — DS-02", buttonExamples())
|
||||
@catalogSection("card", "Card — DS-04", cardExamples())
|
||||
@catalogSection("empty-state", "Empty State — DS-09", emptyStateExamples())
|
||||
@catalogSection("form-field", "Form Field — DS-08", formFieldExamples())
|
||||
@catalogSection("icon-button", "Icon Button — DS-07", iconButtonExamples())
|
||||
@catalogSection("input", "Input — DS-03", inputExamples())
|
||||
@catalogSection("modal", "Modal — DS-06", modalExamples())
|
||||
@catalogSection("select", "Select — DS-10", selectExamples())
|
||||
@catalogSection("table", "Table — DS-11", tableExamples())
|
||||
@catalogSection("textarea", "Textarea — DS-03b", textareaExamples())
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
templ catalogSection(id string, heading string, examples []Example) {
|
||||
<section id={ id } class="catalog-section">
|
||||
<h2 class="catalog-section-heading">{ heading }</h2>
|
||||
for _, ex := range examples {
|
||||
<div class="catalog-example">
|
||||
<p class="catalog-example-title">{ ex.Title }</p>
|
||||
<div class="catalog-example-preview">
|
||||
@ex.Preview
|
||||
</div>
|
||||
if ex.Snippet != "" {
|
||||
<pre class="catalog-example-snippet"><code>{ ex.Snippet }</code></pre>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
407
backend/internal/web/ui/catalog/examples.go
Normal file
407
backend/internal/web/ui/catalog/examples.go
Normal file
|
|
@ -0,0 +1,407 @@
|
|||
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
|
||||
}
|
||||
|
|
@ -137,6 +137,19 @@ worker: db-up
|
|||
S3_USE_PATH_STYLE='{{ s3_use_path_style }}' \
|
||||
go run ./cmd/worker
|
||||
|
||||
# Run the component catalog on localhost:8080/ui-catalog (dev-only, -tags catalog).
|
||||
# Visit http://localhost:8080/ui-catalog to review all 11 component sections.
|
||||
catalog:
|
||||
just generate
|
||||
DATABASE_URL='{{ database_url }}' \
|
||||
S3_ENDPOINT='{{ s3_endpoint }}' \
|
||||
S3_BUCKET='{{ s3_bucket }}' \
|
||||
S3_REGION='{{ s3_region }}' \
|
||||
S3_ACCESS_KEY='{{ s3_access_key }}' \
|
||||
S3_SECRET_KEY='{{ s3_secret_key }}' \
|
||||
S3_USE_PATH_STYLE='{{ s3_use_path_style }}' \
|
||||
go run -tags catalog ./cmd/web
|
||||
|
||||
test:
|
||||
just generate
|
||||
go test ./...
|
||||
|
|
|
|||
Loading…
Reference in a new issue