Documentation
¶
Overview ¶
Package linkwell provides types and helpers for HATEOAS-style hypermedia controls, link relations (RFC 8288), and navigation primitives. All types are pure data descriptors — they carry no rendering logic and can be consumed by any template engine (templ, html/template, etc.) or serialized to JSON.
Index ¶
- Constants
- Variables
- func AllLinks() map[string][]LinkRelation
- func ComputeTotalPages(totalItems, perPage int) int
- func FromNav(href, from string) string
- func FromParam(mask uint64) string
- func FromQueryString(mask uint64) string
- func Hub(centerPath, centerTitle string, spokes ...RelEntry)
- func Link(source, rel, target, title string)
- func LinkHeader(links []LinkRelation) string
- func LoadStoredLink(source string, r LinkRelation)
- func ParseFromParam(raw string) uint64
- func RegisterFrom(bit FromBit, crumb Breadcrumb)
- func RemoveLink(source, href, rel string) bool
- func ResetForTesting()
- func Ring(name string, members ...RelEntry)
- func TitleFromPath(path string) string
- type Breadcrumb
- type BulkActionCfg
- type Control
- func BackButton(label string) Control
- func BulkActions(cfg BulkActionCfg) []Control
- func CatalogRowAction(detailURL, detailRowTarget string) Control
- func ConfirmAction(label string, method HxMethod, url, target, confirmMsg string) Control
- func DismissButton(label string) Control
- func EmptyStateAction(label, createURL, target string) Control
- func ErrorControlsForStatus(statusCode int, opts ErrorControlOpts) []Control
- func ForbiddenControls() []Control
- func FormActions(cancelHref string) []Control
- func GoHomeButton(label, homeURL, target string) Control
- func HTMXAction(label string, req HxRequestConfig) Control
- func InternalErrorControls(opts ErrorControlOpts) []Control
- func NewRowFormActions(cfg RowFormActionCfg) []Control
- func NotFoundControls(homeURL string) []Control
- func PaginationControls(info PageInfo) []Control
- func RedirectLink(label, href string) Control
- func ReportIssueButton(label, requestID string) Control
- func ResourceActions(cfg ResourceActionCfg) []Control
- func RetryButton(label string, method HxMethod, url, target string) Control
- func RowActions(cfg RowActionCfg) []Control
- func RowFormActions(cfg RowFormActionCfg) []Control
- func ServiceErrorControls(opts ErrorControlOpts) []Control
- func TableRowActions(cfg TableRowActionCfg) []Control
- func UnauthorizedControls(loginURL string) []Control
- type ControlKind
- type ControlVariant
- type ErrorContext
- type ErrorControlOpts
- type FilterBar
- type FilterField
- func CheckboxField(name, label, value string) FilterField
- func DateField(name, label, value string) FilterField
- func RangeField(name, label, value, min, max, step string) FilterField
- func SearchField(name, placeholder, value string) FilterField
- func SelectField(name, label, value string, options []FilterOption) FilterField
- type FilterGroup
- type FilterKind
- type FilterOption
- type FromBit
- type HTTPError
- type HubEntry
- type HxMethod
- type HxRequestConfig
- type Icon
- type LinkIssue
- type LinkRelation
- type ModalButton
- type ModalButtonRole
- type ModalButtonSet
- type ModalConfig
- type NavConfig
- type NavItem
- type PageInfo
- type RelEntry
- type Renderable
- type ResourceActionCfg
- type RowActionCfg
- type RowFormActionCfg
- type SitemapEntry
- type SortDir
- type Step
- type StepStatus
- type StepperConfig
- type SwapMode
- type TabConfig
- type TabItem
- type TableCol
- type TableRowActionCfg
- type Toast
- type ToastVariant
Examples ¶
- BackButton
- BreadcrumbsFromLinks
- BreadcrumbsFromPath
- BulkActions
- ComputeTotalPages
- ConfirmAction
- Control.WithSwap
- DismissButton
- EmptyStateAction
- ErrorContext.WithControls
- ErrorControlsForStatus
- FormActions
- FromNav
- FromQueryString
- GoHomeButton
- HTMXAction
- Hub
- Hubs
- HxGet
- HxRequestConfig.Attrs
- Link
- LinkHeader
- LinksFor
- LoadStoredLink
- NavItemFromControl
- NewFilterBar
- NewFilterGroup
- NewHTTPError
- PageInfo.URLForPage
- PaginationControls
- ParseFromParam
- RedirectLink
- RegisterFrom
- Rel
- RelatedLinksFor
- RemoveLink
- ReportIssueModal
- ResourceActions
- RetryButton
- Ring
- RowActions
- SelectOptions
- SetActiveNavItem
- SetActiveNavItemPrefix
- SortableCol
- TitleFromPath
Constants ¶
const ( IncludeClosestTR = "closest tr" IncludeClosestForm = "closest form" )
Include selector constants for HxRequestConfig.Include.
const ( LabelEdit = "Edit" LabelDelete = "Delete" LabelDeleteSelected = "Delete Selected" LabelSave = "Save" LabelCancel = "Cancel" LabelDetails = "Details" LabelActivate = "Activate" LabelDeactivate = "Deactivate" LabelGoBack = "Go Back" LabelGoHome = "Go Home" LabelRetry = "Retry" LabelDismiss = "Close" LabelLogIn = "Log In" LabelReportIssue = "Report Issue" )
Default labels used by pattern factories. Override by building Controls directly.
const ( RelRelated = "related" RelUp = "up" RelSelf = "self" RelAlternate = "alternate" RelCanonical = "canonical" RelFirst = "first" RelLast = "last" RelNext = "next" RelPrev = "prev" RelCollection = "collection" RelItem = "item" )
Common IANA link relation types (RFC 8288). See https://www.iana.org/assignments/link-relations/ for the full registry.
const ( LabelPrevious = "Previous" LabelNext = "Next" LabelSubmit = "Submit" )
Default labels for stepper navigation controls.
const ( ParamSort = "sort" ParamDir = "dir" ParamPage = "page" )
Query parameter keys used by sort and pagination helpers.
const ( PaginationFirst = "«" PaginationPrev = "‹" PaginationNext = "›" PaginationLast = "»" )
Pagination navigation labels.
const BreadcrumbLabelHome = "Home"
BreadcrumbLabelHome is the default label for the root breadcrumb segment.
const (
ConfirmDeleteSelected = "Delete selected items?"
)
Default confirm messages used by pattern factories.
const DefaultErrorStatusTarget = "#error-status"
DefaultErrorStatusTarget is the CSS selector for the error status container used by OOB placements and response builders to target the error panel.
const DefaultFilterFormID = "filter-form"
DefaultFilterFormID is the HTML form id used by NewFilterBar.
const (
TargetBody = "body"
)
Default HTMX target selectors used by pattern factories.
Variables ¶
var ( ModalOK = ModalButtonSet{ {Label: "OK", Role: ModalRolePrimary, Variant: VariantPrimary}, } ModalYesNo = ModalButtonSet{ {Label: "No", Role: ModalRoleCancel, Variant: VariantGhost}, {Label: "Yes", Role: ModalRolePrimary, Variant: VariantPrimary}, } ModalSaveCancel = ModalButtonSet{ {Label: LabelCancel, Role: ModalRoleCancel, Variant: VariantGhost}, {Label: LabelSave, Role: ModalRolePrimary, Variant: VariantPrimary}, } ModalSaveCancelReset = ModalButtonSet{ {Label: "Reset", Role: ModalRoleSecondary, Variant: VariantSecondary}, {Label: LabelCancel, Role: ModalRoleCancel, Variant: VariantGhost}, {Label: LabelSave, Role: ModalRolePrimary, Variant: VariantPrimary}, } ModalSubmitCancel = ModalButtonSet{ {Label: LabelCancel, Role: ModalRoleCancel, Variant: VariantGhost}, {Label: "Submit", Role: ModalRolePrimary, Variant: VariantPrimary}, } ModalConfirmCancel = ModalButtonSet{ {Label: LabelCancel, Role: ModalRoleCancel, Variant: VariantGhost}, {Label: "Confirm", Role: ModalRolePrimary, Variant: VariantDanger}, } ModalDeleteCancel = ModalButtonSet{ {Label: LabelCancel, Role: ModalRoleCancel, Variant: VariantGhost}, {Label: LabelDelete, Role: ModalRolePrimary, Variant: VariantDanger}, } )
Preset modal button sets.
Functions ¶
func AllLinks ¶
func AllLinks() map[string][]LinkRelation
AllLinks returns a snapshot of all registered link relations grouped by source path. The returned map and slices are copies, safe to modify. Useful for admin dashboards or debug pages that inspect the link graph.
func ComputeTotalPages ¶
ComputeTotalPages calculates the number of pages needed to display totalItems at the given perPage size. Always returns at least 1.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
fmt.Println(linkwell.ComputeTotalPages(100, 25))
fmt.Println(linkwell.ComputeTotalPages(101, 25))
fmt.Println(linkwell.ComputeTotalPages(0, 25))
}
Output: 4 5 1
func FromNav ¶
FromNav appends the ?from= parameter to a href, preserving any existing query string. Returns href unchanged if from is empty. Use in templates to forward breadcrumb context to outbound links.
func FromParam ¶
FromParam formats a bitmask as a decimal string suitable for ?from= query parameter values.
func FromQueryString ¶
FromQueryString returns "from=N" formatted for inclusion in URL query strings. Returns an empty string if mask is 0 (no origin context).
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
fmt.Println(linkwell.FromQueryString(3))
fmt.Println(linkwell.FromQueryString(0))
}
Output: from=3
func Hub ¶
Hub registers a star topology where a center page links to all spokes via rel="related", and each spoke links back to the center via rel="up". Spokes do not link to each other. The centerTitle is used as the Group field on all links and as the hub label returned by Hubs. Registration is safe for concurrent use.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
linkwell.ResetForTesting()
linkwell.Hub("/admin", "Admin",
linkwell.Rel("/admin/users", "Users"),
linkwell.Rel("/admin/roles", "Roles"),
)
// Center links to spokes via rel="related"
centerLinks := linkwell.LinksFor("/admin", "related")
fmt.Printf("center has %d spokes\n", len(centerLinks))
// Spokes link back to center via rel="up"
upLinks := linkwell.LinksFor("/admin/users", "up")
fmt.Printf("spoke -> %s (%s)\n", upLinks[0].Href, upLinks[0].Title)
}
Output: center has 2 spokes spoke -> /admin (Admin)
func Link ¶
func Link(source, rel, target, title string)
Link registers a directional relationship from a source path to a target. The rel parameter should be an IANA link relation type (e.g., RelRelated, RelUp, "collection"). For rel="related", the inverse link is automatically created so the relationship is symmetric — both pages will see each other in their link sets. Registration is safe for concurrent use.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
linkwell.ResetForTesting()
// rel="related" is symmetric: both sides are registered automatically.
linkwell.Link("/inventory", "related", "/warehouses", "Warehouses")
fmt.Println(linkwell.LinksFor("/inventory")[0].Title)
fmt.Println(linkwell.LinksFor("/warehouses")[0].Title)
}
Output: Warehouses Inventory
func LinkHeader ¶
func LinkHeader(links []LinkRelation) string
LinkHeader formats a slice of LinkRelation values as an RFC 8288 Link header string. Each relation is rendered as `<href>; rel="type"; title="label"`, joined by commas. The title parameter is omitted when empty and properly escaped per RFC 7230 quoted-string rules. Returns an empty string if links is empty.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
links := []linkwell.LinkRelation{
{Rel: "related", Href: "/warehouses", Title: "Warehouses"},
{Rel: "up", Href: "/admin", Title: "Admin"},
}
fmt.Println(linkwell.LinkHeader(links))
}
Output: </warehouses>; rel="related"; title="Warehouses", </admin>; rel="up"; title="Admin"
func LoadStoredLink ¶
func LoadStoredLink(source string, r LinkRelation)
LoadStoredLink adds a single link relation from an external source (e.g., a database or configuration file) into the in-memory registry. Duplicate links (same source, href, and rel) are silently skipped.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
linkwell.ResetForTesting()
linkwell.LoadStoredLink("/projects/42", linkwell.LinkRelation{
Rel: "related", Href: "/teams/7", Title: "Backend Team",
})
links := linkwell.LinksFor("/projects/42")
fmt.Println(links[0].Title)
}
Output: Backend Team
func ParseFromParam ¶
ParseFromParam parses the ?from= query parameter string as a uint64 bitmask. Returns 0 if the input is empty or not a valid unsigned integer.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
mask := linkwell.ParseFromParam("3")
fmt.Println(mask)
fmt.Println(linkwell.ParseFromParam(""))
}
Output: 3 0
func RegisterFrom ¶
func RegisterFrom(bit FromBit, crumb Breadcrumb)
RegisterFrom registers a breadcrumb at the given bit position. Call during route initialization. Bit 0 is pre-registered as Home. If the bit is already registered, the previous entry is replaced.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
linkwell.RegisterFrom(linkwell.FromDashboard, linkwell.Breadcrumb{
Label: "Dashboard", Href: "/dashboard",
})
crumbs := linkwell.ResolveFromMask(linkwell.FromHome | linkwell.FromDashboard)
for _, c := range crumbs {
fmt.Println(c.Label)
}
}
Output: Home Dashboard
func RemoveLink ¶
RemoveLink removes the first link relation matching source, href, and rel from the in-memory registry. Returns true if a link was found and removed, false if no match was found.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
linkwell.ResetForTesting()
linkwell.LoadStoredLink("/a", linkwell.LinkRelation{
Rel: "related", Href: "/b", Title: "B",
})
ok := linkwell.RemoveLink("/a", "/b", "related")
fmt.Println(ok)
fmt.Println(len(linkwell.LinksFor("/a")))
}
Output: true 0
func ResetForTesting ¶
func ResetForTesting()
ResetForTesting clears all entries from the global link, hub, and breadcrumb-origin registries. The Home breadcrumb (bit 0) is re-registered automatically. Intended for use in test setup/teardown only — not safe to call in production while handlers may be reading the registry. In parallel tests, call ResetForTesting at the start of each subtest and register it with t.Cleanup to avoid cross-test pollution.
func Ring ¶
Ring registers symmetric rel="related" links between all members, creating a fully-connected group where every member links to every other member. The name parameter is stored as the Group field on each LinkRelation for UI grouping. Duplicate links are skipped. Registration is safe for concurrent use.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
linkwell.ResetForTesting()
linkwell.Ring("Logistics",
linkwell.Rel("/inventory", "Inventory"),
linkwell.Rel("/warehouses", "Warehouses"),
linkwell.Rel("/shipments", "Shipments"),
)
links := linkwell.LinksFor("/inventory")
for _, l := range links {
fmt.Printf("%s -> %s (group=%s)\n", l.Rel, l.Title, l.Group)
}
}
Output: related -> Warehouses (group=Logistics) related -> Shipments (group=Logistics)
func TitleFromPath ¶
TitleFromPath derives a human-readable title from the last segment of a URL path. Hyphens are replaced with spaces and each word is title-cased. Examples: "/demo/inventory" -> "Inventory", "/admin/error-traces" -> "Error Traces".
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
fmt.Println(linkwell.TitleFromPath("/demo/inventory"))
fmt.Println(linkwell.TitleFromPath("/admin/error-traces"))
}
Output: Inventory Error Traces
Types ¶
type Breadcrumb ¶
type Breadcrumb struct {
// Label is the display text for this breadcrumb segment.
Label string
// Href is the navigation target. Empty for the terminal (current page) segment.
Href string
}
Breadcrumb is one segment of a breadcrumb trail. When Href is empty, the segment represents the current page and should be rendered as plain text rather than a clickable link.
func BreadcrumbsFromLinks ¶
func BreadcrumbsFromLinks(path string) []Breadcrumb
BreadcrumbsFromLinks walks the rel="up" chain from path to build a breadcrumb trail. The trail starts with Home, includes each ancestor found via rel="up" links, and ends with the current page (empty Href). Returns nil if no rel="up" links are registered for the path.
When the exact path has no rel="up" links, BreadcrumbsFromLinks strips the last path segment and retries up the hierarchy until a registered path is found. Intermediate segments between the matched ancestor and the original path are included as breadcrumbs. This allows child pages like /admin/groups/new to inherit breadcrumbs from /admin/groups. Cycle-safe.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
linkwell.ResetForTesting()
linkwell.Hub("/admin", "Admin",
linkwell.Rel("/admin/users", "Users"),
)
crumbs := linkwell.BreadcrumbsFromLinks("/admin/users")
for _, c := range crumbs {
if c.Href == "" {
fmt.Printf("[%s]\n", c.Label)
} else {
fmt.Printf("%s (%s)\n", c.Label, c.Href)
}
}
}
Output: Home (/) Admin (/admin) [Users]
func BreadcrumbsFromPath ¶
func BreadcrumbsFromPath(path string, labels map[int]string) []Breadcrumb
BreadcrumbsFromPath generates a breadcrumb trail from a URL path by splitting on "/" and title-casing each segment. The labels map overrides auto-generated labels by segment index (0-based, not counting the Home crumb) — use this to replace opaque IDs with human-readable names. The terminal segment always has an empty Href (rendered as text, not a link).
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
crumbs := linkwell.BreadcrumbsFromPath("/users/42/edit", map[int]string{
1: "Jane Doe",
})
for _, c := range crumbs {
if c.Href == "" {
fmt.Printf("[%s]\n", c.Label)
} else {
fmt.Printf("%s (%s)\n", c.Label, c.Href)
}
}
}
Output: Home (/) Users (/users) Jane Doe (/users/42) [Edit]
func ResolveFromMask ¶
func ResolveFromMask(mask uint64) []Breadcrumb
ResolveFromMask decodes a bitmask into an ordered breadcrumb trail by checking each registered bit position. Only registered bits are included; unregistered bits are silently ignored. Home (bit 0) is always included regardless of the mask value. Entries are ordered by bit position (lowest first).
func ResolveFromMaskWithPath ¶ added in v0.2.4
func ResolveFromMaskWithPath(mask uint64, path string, from string) []Breadcrumb
ResolveFromMaskWithPath combines bitmask-resolved breadcrumbs with path-derived crumbs (using the walk-up behavior of BreadcrumbsFromLinks), deduplicating by base path (query parameters stripped for comparison). The from parameter is forwarded as a ?from= query parameter on intermediate path crumb links. Returns nil if path produces no breadcrumbs.
type BulkActionCfg ¶
type BulkActionCfg struct {
DeleteURL string // DELETE URL for bulk deletion.
ActivateURL string // PUT URL for bulk activation.
DeactivateURL string // PUT URL for bulk deactivation.
TableTarget string // CSS selector for the table to replace after the operation.
CheckboxSelector string // CSS selector for row checkboxes (e.g., ".user-checkbox").
ErrorTarget string // CSS selector for inline error display.
}
BulkActionCfg configures toolbar controls for batch operations on checkbox-selected table rows. Each URL is optional — omit to hide that action. The CheckboxSelector is the CSS selector for row checkboxes included via hx-include.
type Control ¶
type Control struct {
// HxRequest carries the HTMX request attributes (method, URL, target, etc.).
HxRequest HxRequestConfig
// Kind determines how the template renders this control (button, link, etc.).
Kind ControlKind
// Label is the user-visible text for the control.
Label string
// Href is the URL for link-type controls (ControlKindLink, ControlKindHome).
Href string
// Variant sets the visual emphasis (primary, danger, ghost, etc.).
Variant ControlVariant
// Confirm is an optional hx-confirm message shown before the action executes.
Confirm string
// Icon is an optional icon name rendered alongside the label.
Icon Icon
// PushURL is set on navigation controls to update the browser URL via hx-push-url.
PushURL string
// Swap overrides the default hx-swap strategy for this control.
Swap SwapMode
// Disabled renders the control in a non-interactive state.
Disabled bool
// ErrorTarget overrides the parent hx-target-error so error responses render
// inline near this control rather than in a global error container.
ErrorTarget string
// ModalID ties this control to a specific modal dialog.
ModalID string
}
Control is a pure-data descriptor for a single hypermedia affordance — a button, link, or action that a user can take. Templates consume controls to render the appropriate HTML elements with the correct HTMX attributes, confirmation dialogs, icons, and visual styles. Controls are value types; use the With* methods to derive modified copies.
func BackButton ¶
BackButton creates a client-side browser history.back() control. No server round-trip occurs — the template renders this as a HyperScript-powered button.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
ctrl := linkwell.BackButton("Go Back")
fmt.Printf("kind=%s label=%s\n", ctrl.Kind, ctrl.Label)
}
Output: kind=back label=Go Back
func BulkActions ¶
func BulkActions(cfg BulkActionCfg) []Control
BulkActions returns toolbar controls for batch operations on checkbox-selected rows. Controls are conditionally included: omit DeleteURL, ActivateURL, or DeactivateURL to hide the corresponding action.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
controls := linkwell.BulkActions(linkwell.BulkActionCfg{
DeleteURL: "/users/bulk-delete",
ActivateURL: "/users/bulk-activate",
TableTarget: "#user-table",
CheckboxSelector: ".user-checkbox",
})
for _, c := range controls {
fmt.Printf("%s variant=%s\n", c.Label, c.Variant)
}
}
Output: Delete Selected variant=danger Activate variant=secondary
func CatalogRowAction ¶
CatalogRowAction returns a ghost-variant Details button that fills an adjacent placeholder row via innerHTML swap. Use for catalog/listing views where clicking a row loads detail content into an expandable area below it.
func ConfirmAction ¶
ConfirmAction creates a danger-variant HTMX button with an hx-confirm confirmation dialog. Use for destructive operations (delete, archive) where the user should explicitly confirm before the request is sent.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
ctrl := linkwell.ConfirmAction("Delete", linkwell.HxMethodDelete, "/users/42", "#list", "Delete?")
fmt.Printf("kind=%s variant=%s confirm=%s\n", ctrl.Kind, ctrl.Variant, ctrl.Confirm)
}
Output: kind=htmx variant=danger confirm=Delete?
func DismissButton ¶
DismissButton creates a HyperScript-powered close/dismiss control. Typically used to close error banners, notifications, or alert panels.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
ctrl := linkwell.DismissButton("Close")
fmt.Printf("kind=%s label=%s\n", ctrl.Kind, ctrl.Label)
}
Output: kind=dismiss label=Close
func EmptyStateAction ¶
EmptyStateAction returns a single primary call-to-action control for empty list states (e.g., "Create First User" when a table has no rows).
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
ctrl := linkwell.EmptyStateAction("Create First User", "/users/new", "#content")
fmt.Printf("%s kind=%s variant=%s\n", ctrl.Label, ctrl.Kind, ctrl.Variant)
}
Output: Create First User kind=htmx variant=primary
func ErrorControlsForStatus ¶
func ErrorControlsForStatus(statusCode int, opts ErrorControlOpts) []Control
ErrorControlsForStatus dispatches to the appropriate control builder based on HTTP status code. Returns a [Dismiss] control for unrecognized status codes. Use in generic error-handling middleware.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
controls := linkwell.ErrorControlsForStatus(404, linkwell.ErrorControlOpts{
HomeURL: "/",
})
for _, c := range controls {
fmt.Printf("%s kind=%s\n", c.Label, c.Kind)
}
}
Output: Go Back kind=back Go Home kind=home
func ForbiddenControls ¶
func ForbiddenControls() []Control
ForbiddenControls returns [Back, Dismiss] controls for a 403 response.
func FormActions ¶
FormActions returns [Save, Cancel] controls for form footers. The Save button uses hx-include="closest form" to submit the form data; the parent form element must carry the hx-post or hx-put attribute that drives submission. The Cancel button is a plain link to cancelHref.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
controls := linkwell.FormActions("/users")
for _, c := range controls {
fmt.Printf("%s kind=%s\n", c.Label, c.Kind)
}
}
Output: Save kind=htmx Cancel kind=link
func GoHomeButton ¶
GoHomeButton creates a navigation control that loads the home page via HTMX GET and pushes homeURL to browser history via hx-push-url.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
ctrl := linkwell.GoHomeButton("Go Home", "/", "body")
fmt.Printf("kind=%s href=%s pushURL=%s\n", ctrl.Kind, ctrl.Href, ctrl.PushURL)
}
Output: kind=home href=/ pushURL=/
func HTMXAction ¶
func HTMXAction(label string, req HxRequestConfig) Control
HTMXAction creates an HTMX button with the supplied request config. Use when the preset factory functions do not fit — this gives full control over the HTMX attributes while still producing a typed Control.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
ctrl := linkwell.HTMXAction("Archive", linkwell.HxPost("/users/42/archive", "#content"))
fmt.Printf("kind=%s method=%s url=%s\n", ctrl.Kind, ctrl.HxRequest.Method, ctrl.HxRequest.URL)
}
Output: kind=htmx method=post url=/users/42/archive
func InternalErrorControls ¶
func InternalErrorControls(opts ErrorControlOpts) []Control
InternalErrorControls returns [Retry, Dismiss] controls for a 500 response. The Retry button is omitted if opts.RetryURL is empty.
func NewRowFormActions ¶
func NewRowFormActions(cfg RowFormActionCfg) []Control
NewRowFormActions returns Save + Cancel controls for a new-item inline form row. Save uses hx-post (instead of hx-put) with hx-include="closest tr". Controls are conditionally included: omit SaveURL to hide Save, omit CancelURL to hide Cancel.
func NotFoundControls ¶
NotFoundControls returns [Back] and optionally [GoHome] controls for a 404 response. Pass an empty homeURL to omit the GoHome button.
func PaginationControls ¶
PaginationControls generates the control slice for a pagination bar. Returns nil if TotalPages <= 1 (no pagination needed). The returned controls follow a [First][Prev][page window][Next][Last] layout. The current page is rendered as a disabled primary-variant control; boundary controls (First/Prev at page 1, Next/Last at last page) are disabled.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
info := linkwell.PageInfo{
BaseURL: "/users",
Page: 2,
PerPage: 25,
TotalItems: 100,
TotalPages: 4,
Target: "#user-table",
}
controls := linkwell.PaginationControls(info)
for _, c := range controls {
fmt.Printf("label=%-3s disabled=%v\n", c.Label, c.Disabled)
}
}
Output: label=« disabled=false label=‹ disabled=false label=1 disabled=false label=2 disabled=true label=3 disabled=false label=4 disabled=false label=› disabled=false label=» disabled=false
func RedirectLink ¶
RedirectLink creates a plain anchor (<a>) control for same-tab navigation.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
ctrl := linkwell.RedirectLink("View Profile", "/users/42")
fmt.Printf("kind=%s href=%s\n", ctrl.Kind, ctrl.Href)
}
Output: kind=link href=/users/42
func ReportIssueButton ¶
ReportIssueButton creates an HTMX button that fetches the Report Issue modal via GET to /report-issue (or /report-issue/{requestID} if provided). The server returns the modal fragment which auto-opens on load.
func ResourceActions ¶
func ResourceActions(cfg ResourceActionCfg) []Control
ResourceActions returns Edit + Delete controls for resource detail pages. Controls are conditionally included: omit EditURL to hide Edit, omit DeleteURL to hide Delete. Returns an empty slice if both URLs are empty.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
controls := linkwell.ResourceActions(linkwell.ResourceActionCfg{
EditURL: "/users/42/edit",
DeleteURL: "/users/42",
ConfirmMsg: "Delete this user?",
Target: "#content",
})
for _, c := range controls {
fmt.Printf("%s kind=%s variant=%s\n", c.Label, c.Kind, c.Variant)
}
}
Output: Edit kind=htmx variant=secondary Delete kind=htmx variant=danger
func RetryButton ¶
RetryButton creates a primary-variant HTMX button that re-issues a failed request. Typically used in error states to let the user retry the operation that produced the error.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
ctrl := linkwell.RetryButton("Retry", linkwell.HxMethodGet, "/api/data", "#content")
fmt.Printf("kind=%s variant=%s\n", ctrl.Kind, ctrl.Variant)
}
Output: kind=retry variant=primary
func RowActions ¶
func RowActions(cfg RowActionCfg) []Control
RowActions returns Edit + Delete controls for a table row where both actions swap the same row target with outerHTML. Controls are conditionally included: omit EditURL to hide Edit, omit DeleteURL to hide Delete.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
controls := linkwell.RowActions(linkwell.RowActionCfg{
EditURL: "/users/42/edit",
DeleteURL: "/users/42",
RowTarget: "#row-42",
ConfirmMsg: "Delete?",
})
for _, c := range controls {
fmt.Printf("%s swap=%s\n", c.Label, c.Swap)
}
}
Output: Edit swap=outerHTML Delete swap=outerHTML
func RowFormActions ¶
func RowFormActions(cfg RowFormActionCfg) []Control
RowFormActions returns Save + Cancel controls for an inline edit row. Save uses hx-put with hx-include="closest tr" to submit the row's form fields. Controls are conditionally included: omit SaveURL to hide Save, omit CancelURL to hide Cancel.
func ServiceErrorControls ¶
func ServiceErrorControls(opts ErrorControlOpts) []Control
ServiceErrorControls returns [Retry, Dismiss] controls for a 503 response. The Retry button is omitted if opts.RetryURL is empty.
func TableRowActions ¶
func TableRowActions(cfg TableRowActionCfg) []Control
TableRowActions returns Edit + Delete controls where Edit swaps the row and Delete replaces the entire table container. Controls are conditionally included: omit EditURL to hide Edit, omit DeleteURL to hide Delete.
func UnauthorizedControls ¶
UnauthorizedControls returns [Log In, Dismiss] controls for a 401 response. The Log In button is omitted if loginURL is empty.
func (Control) WithConfirm ¶
WithConfirm returns a copy of the control with the given hx-confirm message.
func (Control) WithDisabled ¶
WithDisabled returns a copy of the control with the given disabled state.
func (Control) WithErrorTarget ¶
WithErrorTarget returns a copy of the control with the given hx-target-error CSS selector, overriding any parent error target so error responses render inline near this control.
func (Control) WithSwap ¶
WithSwap returns a copy of the control with the given HTMX swap strategy.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
ctrl := linkwell.RetryButton("Retry", linkwell.HxMethodGet, "/api", "#c").
WithSwap(linkwell.SwapOuterHTML).
WithVariant(linkwell.VariantDanger).
WithIcon(linkwell.IconCheck).
WithConfirm("Sure?").
WithDisabled(true).
WithErrorTarget("#err")
fmt.Printf("swap=%s variant=%s icon=%s confirm=%s disabled=%v errTarget=%s\n",
ctrl.Swap, ctrl.Variant, ctrl.Icon, ctrl.Confirm, ctrl.Disabled, ctrl.ErrorTarget)
}
Output: swap=outerHTML variant=danger icon=check confirm=Sure? disabled=true errTarget=#err
func (Control) WithVariant ¶
func (c Control) WithVariant(v ControlVariant) Control
WithVariant returns a copy of the control with the given visual variant.
type ControlKind ¶
type ControlKind string
ControlKind identifies the rendering strategy a template should use for a Control. Each kind implies a specific HTML element and behavior pattern.
const ( // ControlKindRetry renders an HTMX button that re-issues a failed request. ControlKindRetry ControlKind = "retry" // ControlKindLink renders a plain HTML anchor (<a>) element. ControlKindLink ControlKind = "link" // ControlKindHTMX renders a button with HTMX attributes spread from HxRequest. ControlKindHTMX ControlKind = "htmx" // ControlKindDismiss renders a close button powered by HyperScript. ControlKindDismiss ControlKind = "dismiss" // ControlKindBack renders a button that calls browser history.back(). ControlKindBack ControlKind = "back" // ControlKindHome renders a navigation button that redirects to the home page. ControlKindHome ControlKind = "home" // ControlKindReport renders an HTMX button that fetches a report modal. ControlKindReport ControlKind = "report" )
type ControlVariant ¶
type ControlVariant string
ControlVariant determines the visual emphasis of a Control. Templates map variants to CSS classes (e.g., DaisyUI btn-primary, btn-ghost). The zero value ("") renders as secondary, which is a safe default.
const ( // VariantPrimary is the filled call-to-action style. VariantPrimary ControlVariant = "primary" // VariantSecondary is the supporting action style (default/zero value). VariantSecondary ControlVariant = "secondary" // VariantDanger is used for destructive actions. VariantDanger ControlVariant = "danger" // VariantGhost is a low-emphasis style. VariantGhost ControlVariant = "ghost" // VariantLink renders with a text-only link appearance. VariantLink ControlVariant = "link" )
type ErrorContext ¶
type ErrorContext struct {
// Err is the underlying Go error (not shown to users, used for logging).
Err error
// Message is the user-facing error message.
Message string
// Route is the request path that produced the error (for logging/reporting).
Route string
// RequestID is the correlation ID for the request (passed to ReportIssueButton).
RequestID string
// OOBTarget is the CSS selector for an HTMX OOB swap target. When set, the
// error renders as an out-of-band fragment rather than the main response.
OOBTarget string
// OOBSwap is the hx-swap-oob strategy (e.g., "innerHTML", "outerHTML").
OOBSwap string
// Controls is the set of hypermedia affordances rendered with the error
// (e.g., Retry, Back, Dismiss buttons).
Controls []Control
// StatusCode is the HTTP status code for the error response.
StatusCode int
// Closable indicates whether the error panel should show a close/dismiss button.
Closable bool
// Theme is the DaisyUI theme for full-page error renders. Empty defaults to "dark".
Theme string
}
ErrorContext carries all information needed to render a hypermedia error response with action controls. It flows through the error handling pipeline from handler to middleware to template. Use WithControls and WithOOB to build up the context fluently.
func (ErrorContext) WithControls ¶
func (ec ErrorContext) WithControls(controls ...Control) ErrorContext
WithControls returns a copy of the ErrorContext with the given controls appended to the existing set. The original is not modified.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
ec := linkwell.ErrorContext{
StatusCode: 500,
Message: "Database error",
}
ec = ec.WithControls(linkwell.DismissButton("Close"))
ec = ec.WithOOB("#error-status", "innerHTML")
fmt.Printf("controls=%d oobTarget=%s\n", len(ec.Controls), ec.OOBTarget)
}
Output: controls=1 oobTarget=#error-status
func (ErrorContext) WithOOB ¶
func (ec ErrorContext) WithOOB(target, swap string) ErrorContext
WithOOB returns a copy of the ErrorContext configured for HTMX out-of-band swap rendering. Set target to the CSS selector and swap to the hx-swap-oob strategy (e.g., "innerHTML").
type ErrorControlOpts ¶
type ErrorControlOpts struct {
// RetryMethod is the HTTP method for the retry button. Defaults to GET if empty.
RetryMethod HxMethod
// RetryURL is the request URL to retry (typically the current request URL).
RetryURL string
// RetryTarget is the CSS selector for hx-target on the retry button.
RetryTarget string
// HomeURL is the home page URL for the GoHome button (typically "/").
HomeURL string
// LoginURL is the login page URL for the Log In button (typically "/login").
LoginURL string
}
ErrorControlOpts carries context for building status-code-specific control sets. Zero values are safe: missing fields cause the corresponding control to be omitted from the result. For example, omitting RetryURL skips the Retry button; omitting HomeURL skips the GoHome button.
type FilterBar ¶
type FilterBar struct {
// ID is the HTML form id attribute. Defaults to DefaultFilterFormID ("filter-form")
// when created via NewFilterBar.
ID string
// Action is the hx-get endpoint the form submits to (e.g., "/users").
Action string
// Target is the hx-target CSS selector (e.g., "#table-container").
Target string
// Fields is the ordered list of filter inputs rendered in the form.
Fields []FilterField
}
FilterBar is the descriptor for a complete filter form. The form element carries an HTML id so that pagination and sort links can use hx-include="#filter-form" to forward filter state with their requests.
func NewFilterBar ¶
func NewFilterBar(action, target string, fields ...FilterField) FilterBar
NewFilterBar creates a FilterBar with the default form ID and the given action endpoint, HTMX target, and fields.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
bar := linkwell.NewFilterBar("/users", "#user-table",
linkwell.SearchField("q", "Search users...", "alice"),
linkwell.SelectField("status", "Status", "active", linkwell.SelectOptions("active",
"", "All",
"active", "Active",
"inactive", "Inactive",
)),
)
fmt.Printf("id=%s action=%s fields=%d\n", bar.ID, bar.Action, len(bar.Fields))
fmt.Printf("search value=%s\n", bar.Fields[0].Value)
}
Output: id=filter-form action=/users fields=2 search value=alice
type FilterField ¶
type FilterField struct {
// HTMXAttrs holds optional HTMX attributes for this field (e.g., for
// cascading filter updates triggered by hx-get on change).
HTMXAttrs map[string]string
// Kind determines the HTML input type rendered by the template.
Kind FilterKind
// Name is the form field name (maps to the query parameter key).
Name string
// Label is the visible label text. Used by select, range, checkbox, and date.
Label string
// Placeholder is hint text for search inputs.
Placeholder string
// Value is the current serialized value from the query string.
Value string
// Min is the minimum value for range inputs.
Min string
// Max is the maximum value for range inputs.
Max string
// Step is the increment step for range inputs.
Step string
// Options is the list of choices for select inputs.
Options []FilterOption
// Disabled renders the field in a non-interactive state.
Disabled bool
}
FilterField is a pure-data descriptor for a single filter input. Value always holds the current serialized value as a string regardless of Kind:
- Search/Select/Date: the query parameter value
- Checkbox: "true" when checked, "" when unchecked (param absent)
- Range: the current numeric string; Min/Max/Step bound the slider
func CheckboxField ¶
func CheckboxField(name, label, value string) FilterField
CheckboxField creates a boolean toggle. Pass the raw query parameter value: "true" when checked, "" when unchecked or absent.
func DateField ¶
func DateField(name, label, value string) FilterField
DateField creates a date picker input.
func RangeField ¶
func RangeField(name, label, value, min, max, step string) FilterField
RangeField creates a range slider input bounded by min, max, and step.
func SearchField ¶
func SearchField(name, placeholder, value string) FilterField
SearchField creates a text search input with the given name, placeholder, and current value.
func SelectField ¶
func SelectField(name, label, value string, options []FilterOption) FilterField
SelectField creates a select dropdown with the given options. Use SelectOptions to build the options slice from flat value/label pairs.
type FilterGroup ¶
type FilterGroup struct {
// Bar is the underlying filter bar configuration.
Bar FilterBar
}
FilterGroup wraps a FilterBar with methods for dynamic option updates and OOB (out-of-band) swap rendering. Use when filter options change based on other selections (e.g., cascading dropdowns) and the server needs to push updated select elements back to the page via HTMX OOB swaps.
func NewFilterGroup ¶
func NewFilterGroup(action, target string, fields ...FilterField) FilterGroup
NewFilterGroup creates a FilterGroup with a default-ID FilterBar.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
group := linkwell.NewFilterGroup("/products", "#table",
linkwell.SelectField("cat", "Category", "", nil),
)
group.UpdateOptions("cat", linkwell.SelectOptions("",
"electronics", "Electronics",
"clothing", "Clothing",
))
selects := group.SelectFields()
fmt.Printf("select fields: %d\n", len(selects))
fmt.Printf("options: %d\n", len(selects[0].Options))
}
Output: select fields: 1 options: 2
func (*FilterGroup) SelectFields ¶
func (g *FilterGroup) SelectFields() []FilterField
SelectFields returns only the select-type fields from the bar. Use when rendering OOB swap fragments that update dropdown options without replacing the entire filter form.
func (*FilterGroup) UpdateOptions ¶
func (g *FilterGroup) UpdateOptions(name string, options []FilterOption)
UpdateOptions replaces the options for the named select field. Non-select fields or unrecognized names are silently ignored.
type FilterKind ¶
type FilterKind string
FilterKind identifies the HTML input type for a FilterField. Templates use this to select the appropriate form control (text input, select, range slider, checkbox, or date picker).
const ( FilterKindSearch FilterKind = "search" FilterKindSelect FilterKind = "select" FilterKindRange FilterKind = "range" FilterKindCheckbox FilterKind = "checkbox" FilterKindDate FilterKind = "date" )
Filter kind constants for FilterField.Kind.
type FilterOption ¶
FilterOption is a single option in a select dropdown. Selected is typically set by SelectOptions based on the current query parameter value.
func SelectOptions ¶
func SelectOptions(current string, pairs ...string) []FilterOption
SelectOptions builds a slice of FilterOption from flat value/label pairs. The current parameter is matched against each value to set Selected=true on the matching option. Unpaired trailing values are safely ignored.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
opts := linkwell.SelectOptions("published",
"draft", "Draft",
"published", "Published",
"archived", "Archived",
)
for _, o := range opts {
fmt.Printf("%s selected=%v\n", o.Label, o.Selected)
}
}
Output: Draft selected=false Published selected=true Archived selected=false
type FromBit ¶
type FromBit = uint64
FromBit is a bitmask position for a registered breadcrumb origin. Lower bits render earlier in the trail. Bit 0 is reserved for Home and is always included. Use RegisterFrom to associate a Breadcrumb with a bit position, then pass a bitmask via the ?from= query parameter to reconstruct the trail.
const ( FromHome FromBit = 1 << iota // bit 0 — always shown FromDashboard // bit 1 FromBit2 // bit 2 — available for registration FromBit3 // bit 3 FromBit4 // bit 4 FromBit5 // bit 5 FromBit6 // bit 6 FromBit7 // bit 7 )
Well-known breadcrumb bit positions. Bit 0 (Home) is always included. Register additional bits via RegisterFrom. Bits 2-7 are available for application-specific origins.
type HTTPError ¶
type HTTPError struct {
// EC is the error context carrying the status code, message, and controls.
EC ErrorContext
}
HTTPError is a Go error that wraps an ErrorContext. Return it from handlers and let error-handling middleware detect it via errors.As to render the full hypermedia error response with action controls.
func NewHTTPError ¶
func NewHTTPError(ec ErrorContext) *HTTPError
NewHTTPError wraps an ErrorContext in an HTTPError suitable for returning from a handler. The error handler middleware intercepts it and renders the appropriate hypermedia error response.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
ec := linkwell.ErrorContext{
StatusCode: 404,
Message: "User not found",
}
err := linkwell.NewHTTPError(ec)
fmt.Println(err.Error())
}
Output: HTTP 404: User not found
type HubEntry ¶
type HubEntry struct {
Path string
Title string
Spokes []LinkRelation
}
HubEntry represents a hub center with its spoke links, returned by Hubs for site map or navigation tree rendering. Spokes are sorted alphabetically by title.
func Hubs ¶
func Hubs() []HubEntry
Hubs returns all registered hub centers with their spoke links, sorted by center path. Spokes within each hub are sorted alphabetically by title. Use for rendering site maps or navigation trees.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
linkwell.ResetForTesting()
linkwell.Hub("/admin", "Admin",
linkwell.Rel("/admin/users", "Users"),
linkwell.Rel("/admin/roles", "Roles"),
)
for _, hub := range linkwell.Hubs() {
fmt.Printf("%s (%s)\n", hub.Title, hub.Path)
for _, spoke := range hub.Spokes {
fmt.Printf(" %s %s\n", spoke.Title, spoke.Href)
}
}
}
Output: Admin (/admin) Roles /admin/roles Users /admin/users
type HxMethod ¶
type HxMethod string
HxMethod is the HTTP verb used in an HTMX request attribute. The value maps directly to the hx-get, hx-post, hx-put, hx-patch, or hx-delete attribute name.
type HxRequestConfig ¶
type HxRequestConfig struct {
// Method is the HTTP verb (maps to hx-get, hx-post, etc.).
Method HxMethod
// URL is the request endpoint.
URL string
// Target is the CSS selector for hx-target.
Target string
// Include is the CSS selector for hx-include (e.g., "closest form").
Include string
// Vals is a JSON-encoded string for hx-vals (e.g., `{"status":"active"}`).
Vals string
}
HxRequestConfig describes the HTMX request attributes for a control. It maps to the hx-{method}, hx-target, hx-include, and hx-vals attributes. Use the HxGet, HxPost, HxPut, HxPatch, and HxDelete helpers for common cases, or build directly for full control.
func HxDelete ¶
func HxDelete(url, target string) HxRequestConfig
HxDelete returns a DELETE request config.
func HxGet ¶
func HxGet(url, target string) HxRequestConfig
HxGet returns a GET request config.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
req := linkwell.HxGet("/users", "#user-list")
fmt.Printf("method=%s url=%s target=%s\n", req.Method, req.URL, req.Target)
}
Output: method=get url=/users target=#user-list
func HxPatch ¶
func HxPatch(url, target string) HxRequestConfig
HxPatch returns a PATCH request config.
func (HxRequestConfig) Attrs ¶
func (r HxRequestConfig) Attrs() map[string]string
Attrs converts the config to a map[string]string suitable for attribute spreading in templates. Keys are the HTMX attribute suffixes (e.g., "get", "target", "include", "vals"). Used for interop with NavItem.HTMXAttrs and other consumers that accept generic attribute maps.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
req := linkwell.HxPost("/users", "#list").WithInclude("closest form")
attrs := req.Attrs()
fmt.Println(attrs["post"])
fmt.Println(attrs["target"])
fmt.Println(attrs["include"])
}
Output: /users #list closest form
func (HxRequestConfig) WithInclude ¶
func (r HxRequestConfig) WithInclude(selector string) HxRequestConfig
WithInclude returns a copy of the request config with the given hx-include CSS selector.
type Icon ¶
type Icon string
Icon is a typed icon name for Control.Icon. The built-in constants cover common actions, but any string is valid — templates interpret the value to select the appropriate icon component (e.g., Heroicons, Lucide).
type LinkIssue ¶ added in v0.2.17
type LinkIssue struct {
// Path is the registry path where the issue was detected.
Path string
// Message is a human-readable description of the problem.
Message string
// Kind classifies the issue for programmatic filtering.
// Values: "orphan", "broken_up", "dead_spoke", "unregistered_route", "missing_route".
Kind string
}
LinkIssue describes a structural problem detected in the link registry.
func ValidateAgainstRoutes ¶ added in v0.2.17
ValidateAgainstRoutes checks that all registered link paths exist in the provided route set, and that all routes have a link graph presence. Returns issues for any mismatches.
Detected issues:
- unregistered_route: a path in the link registry has no matching route.
- missing_route: a route has no presence in the link registry (neither as a source nor as a target).
func ValidateGraph ¶ added in v0.2.17
func ValidateGraph() []LinkIssue
ValidateGraph checks the link registry for structural issues and returns a slice of problems found. An empty slice means no issues were detected.
Detected issues:
- orphan: a registered path has no inbound links from any other path.
- broken_up: a rel="up" link targets a path that has no registered links.
- dead_spoke: a hub spoke path has no links registered (empty link set).
type LinkRelation ¶
type LinkRelation struct {
// Rel is the IANA link relation type (e.g., "related", "collection", "up").
// See https://www.iana.org/assignments/link-relations/ for registered values.
Rel string
// Href is the target URL of the linked resource.
Href string
// Title is a human-readable label for the link, suitable for rendering in
// navigation bars, context panels, or breadcrumb trails.
Title string
// Group is an optional grouping label set by Ring or Hub registration.
// Templates can use this to visually cluster related links.
Group string
}
LinkRelation represents a typed relationship between two resources, following the IANA link relation model defined in RFC 8288. Each relation carries a relation type (Rel), target URL (Href), human-readable label (Title), and an optional group name for UI rendering (e.g., the ring or hub name).
func LinksFor ¶
func LinksFor(path string, rels ...string) []LinkRelation
LinksFor returns all registered link relations for the given path. If one or more rel types are provided, only relations matching those types are returned. Returns a copy of the internal slice, safe to modify.
When no links are registered for the exact path, LinksFor walks up the path hierarchy by stripping the last segment and retrying until a registered path is found or the root is reached. This allows child paths like /admin/apps/1 to inherit links from /admin/apps.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
linkwell.ResetForTesting()
linkwell.Link("/page", "related", "/other", "Other")
linkwell.Link("/page", "up", "/parent", "Parent")
// All links for a path
all := linkwell.LinksFor("/page")
fmt.Printf("all: %d\n", len(all))
// Filter by rel type
upOnly := linkwell.LinksFor("/page", "up")
fmt.Printf("up: %s\n", upOnly[0].Href)
}
Output: all: 2 up: /parent
func RelatedLinksFor ¶
func RelatedLinksFor(path string) []LinkRelation
RelatedLinksFor returns only rel="related" links for a path, excluding the path itself and deduplicating by href. Useful for rendering context bars or "See also" panels where self-links would be redundant.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
linkwell.ResetForTesting()
linkwell.Ring("group",
linkwell.Rel("/a", "A"),
linkwell.Rel("/b", "B"),
linkwell.Rel("/c", "C"),
)
peers := linkwell.RelatedLinksFor("/a")
for _, l := range peers {
fmt.Println(l.Title)
}
}
Output: B C
type ModalButton ¶
type ModalButton struct {
// Label is the button text.
Label string
// Role determines the button's behavior (submit, cancel, auxiliary).
Role ModalButtonRole
// Variant sets the visual style of the button.
Variant ControlVariant
}
ModalButton describes a single button inside a modal dialog footer.
type ModalButtonRole ¶
type ModalButtonRole string
ModalButtonRole identifies the semantic role of a button within a modal dialog. Templates use this to determine behavior: primary buttons submit, cancel buttons dismiss, and secondary buttons perform auxiliary actions.
const ( // ModalRolePrimary is the main action button (Submit, Save, Yes, OK, Delete). ModalRolePrimary ModalButtonRole = "primary" // ModalRoleCancel dismisses the modal without action. ModalRoleCancel ModalButtonRole = "cancel" // ModalRoleSecondary is an additional action (Reset, etc.). ModalRoleSecondary ModalButtonRole = "secondary" )
type ModalButtonSet ¶
type ModalButtonSet []ModalButton
ModalButtonSet is an ordered list of buttons for a modal footer. Buttons render left-to-right in the order they appear in the slice. Use the preset variables (ModalOK, ModalYesNo, etc.) for common patterns.
type ModalConfig ¶
type ModalConfig struct {
// ID is the unique HTML id for the modal element.
ID string
// Title is displayed in the modal header.
Title string
// Buttons defines the footer button set.
Buttons ModalButtonSet
// HxPost is the URL for the primary button's hx-post attribute. When empty,
// the primary button uses client-side behavior only (e.g., close on confirm).
HxPost string
// HxTarget is the hx-target CSS selector for the primary button's HTMX
// response.
HxTarget string
// HxSwap is the hx-swap strategy for the primary button's HTMX response.
HxSwap SwapMode
}
ModalConfig describes everything needed to render a modal dialog. The ID must be unique on the page. When HxPost is set, the primary button submits the modal's form content via HTMX POST.
func ReportIssueModal ¶
func ReportIssueModal(requestID string) ModalConfig
ReportIssueModal returns a ModalConfig preconfigured for the Report Issue flow. The modal posts to /report-issue (or /report-issue/{requestID}) with SwapNone so the response triggers a toast/alert without replacing DOM content.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
modal := linkwell.ReportIssueModal("req-abc")
fmt.Printf("id=%s title=%s post=%s swap=%s\n", modal.ID, modal.Title, modal.HxPost, modal.HxSwap)
}
Output: id=report-issue-modal title=Report Issue post=/report-issue/req-abc swap=none
type NavConfig ¶
type NavConfig struct {
Items []NavItem
Promoted *NavItem
// "more" menu. Zero means show all items.
MaxVisible int
AppName string
// the brand slot. Any templ.Component satisfies Renderable. Set to nil to
// use the plain text AppName.
Brand Renderable
// content. Any templ.Component satisfies Renderable. Set to nil to use the
// default layout.
Topbar Renderable
}
NavConfig holds the app-controlled parts of the navigation layout. Zero values are safe defaults: no promoted item, all items visible, no custom brand or topbar slots.
type NavItem ¶
type NavItem struct {
// for items that trigger HTMX requests instead of full navigation.
HTMXAttrs map[string]string
Label string
Href string
Icon string
Children []NavItem
// or SetActiveNavItemPrefix, or manually by the handler.
Active bool
}
NavItem is a server-computed navigation entry. Active state is determined by the handler (via SetActiveNavItem or SetActiveNavItemPrefix), not by client-side JavaScript. Items may have children for dropdown/flyout menus.
func NavItemFromControl ¶
NavItemFromControl converts a Control into a NavItem, mapping Label, Href, Icon, and HxRequest attributes. Useful when a control needs to appear in the navigation bar.
func SetActiveNavItem ¶
SetActiveNavItem sets the Active flag on nav items using exact path matching. A parent item is also marked active if any of its children match. Returns a new slice; the input is not modified.
func SetActiveNavItemPrefix ¶
SetActiveNavItemPrefix sets the Active flag using longest-prefix matching. An item is active if the current path equals its Href or starts with Href followed by "/". Use for section-level navigation where /users should be active when the path is /users/42/edit. A parent is marked active if any child matches. Returns a new slice; the input is not modified.
type PageInfo ¶
type PageInfo struct {
// BaseURL is the current URL (with existing query params preserved).
// URLForPage appends/replaces the page parameter.
BaseURL string
// PageParam overrides the query parameter name for the page number.
// Defaults to "page" when empty.
PageParam string
// Target is the CSS selector for hx-target on pagination links.
Target string
// Include is the CSS selector for hx-include (e.g., "#filter-form").
Include string
// Page is the current 1-based page number.
Page int
// PerPage is the number of items per page.
PerPage int
// TotalItems is the total number of items across all pages.
TotalItems int
// TotalPages is the total number of pages. Use ComputeTotalPages to calculate.
TotalPages int
}
PageInfo carries server-computed pagination state used by PaginationControls to generate the page navigation bar. All fields are set by the handler; the template just passes PageInfo through.
func (PageInfo) URLForPage ¶
URLForPage builds the URL for a specific page number by appending or replacing the page query parameter in BaseURL.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
info := linkwell.PageInfo{BaseURL: "/users?q=foo"}
fmt.Println(info.URLForPage(3))
}
Output: /users?page=3&q=foo
type RelEntry ¶
RelEntry is a path and title pair used as input to Ring and Hub registration. Create instances with the Rel helper function.
func Rel ¶
Rel creates a RelEntry for use with Ring and Hub. This is the preferred constructor for building the member lists passed to those functions.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
entry := linkwell.Rel("/inventory", "Inventory")
fmt.Println(entry.Path, entry.Title)
}
Output: /inventory Inventory
type Renderable ¶ added in v0.2.0
Renderable is a minimal rendering interface satisfied by templ.Component and any type that can render itself to an io.Writer. Using this interface instead of templ.Component directly allows consumers that don't use templ to avoid pulling in the templ module.
type ResourceActionCfg ¶
type ResourceActionCfg struct {
EditURL string // GET URL to fetch the edit form.
DeleteURL string // DELETE URL to remove the resource.
ConfirmMsg string // hx-confirm message for the delete action.
Target string // CSS selector for the content area.
ErrorTarget string // CSS selector for inline error display.
}
ResourceActionCfg configures Edit + Delete controls for resource detail pages (as opposed to table rows). Both actions target the same content area.
type RowActionCfg ¶
type RowActionCfg struct {
EditURL string // GET URL to fetch the edit form for this row.
DeleteURL string // DELETE URL to remove this row.
RowTarget string // CSS selector for the row element (e.g., "#row-42").
ConfirmMsg string // hx-confirm message for the delete action.
ErrorTarget string // CSS selector for inline error display.
}
RowActionCfg configures Edit + Delete controls for a table row. Both the Edit and Delete actions target the same row element (RowTarget) with outerHTML swap.
type RowFormActionCfg ¶
type RowFormActionCfg struct {
SaveURL string // PUT or POST URL for saving the row.
CancelURL string // GET URL to restore the display row.
SaveTarget string // CSS selector for the save response target.
CancelTarget string // CSS selector for the cancel response target.
ErrorTarget string // CSS selector for inline error display.
}
RowFormActionCfg configures Save + Cancel controls for inline table row editing. RowFormActions uses PUT for existing rows; NewRowFormActions uses POST for new rows.
type SitemapEntry ¶ added in v0.2.17
type SitemapEntry struct {
// Path is the URL path of the page.
Path string
// Title is a human-readable label for the page.
Title string
// Parent is the rel="up" parent path, empty for root entries.
Parent string
// Children holds spoke paths when this entry is a hub center.
Children []string
// Group is the ring group name, if any.
Group string
}
SitemapEntry represents a single page in the site hierarchy, derived from the link registry. Each entry captures the page's path, title, parent (via rel="up"), children (hub spokes), and optional ring group membership.
func Sitemap ¶ added in v0.2.17
func Sitemap() []SitemapEntry
Sitemap derives a structured sitemap from the link registry. It collects all registered paths, resolves parent relationships from rel="up" links, populates children from hub registrations, and captures ring group membership. Entries are sorted alphabetically by path.
func SitemapRoots ¶ added in v0.2.17
func SitemapRoots() []SitemapEntry
SitemapRoots returns only sitemap entries that have no parent (empty Parent field). These are the top-level pages in the site hierarchy.
type SortDir ¶
type SortDir string
SortDir indicates the current sort direction for a table column. The zero value (SortNone) means the column is not currently sorted.
type Step ¶ added in v0.2.17
type Step struct {
// Label is the user-visible name for this step.
Label string
// Href is the URL for this step.
Href string
// Status is the current state of this step (pending, active, complete, skipped).
Status StepStatus
// Icon is an optional icon name rendered alongside the step label.
Icon Icon
}
Step describes a single step in a multi-step wizard flow.
type StepStatus ¶ added in v0.2.17
type StepStatus string
StepStatus represents the state of a step in a multi-step wizard flow.
const ( // StepPending indicates the step has not been reached yet. StepPending StepStatus = "pending" // StepActive indicates the step is the current step. StepActive StepStatus = "active" // StepComplete indicates the step has been completed. StepComplete StepStatus = "complete" // StepSkipped indicates the step was skipped. StepSkipped StepStatus = "skipped" )
type StepperConfig ¶ added in v0.2.17
type StepperConfig struct {
// Steps is the ordered list of steps in the wizard.
Steps []Step
// Current is the 0-based index of the active step.
Current int
// Prev is a navigation control pointing to the previous step. Nil if the
// current step is the first step.
Prev *Control
// Next is a navigation control pointing to the next step. Nil if the
// current step is the last step.
Next *Control
// Submit is a submit control shown only on the final step. Nil on all
// other steps.
Submit *Control
}
StepperConfig describes a multi-step wizard flow where the server knows the full step sequence, current position, and completion state. Templates consume this to render step indicators, progress bars, and navigation controls.
func NewStepper ¶ added in v0.2.17
func NewStepper(currentIndex int, steps ...Step) StepperConfig
NewStepper creates a StepperConfig from the given current index and steps. Steps before currentIndex are marked Complete, the step at currentIndex is marked Active, and steps after are marked Pending. Pre-set statuses (e.g., StepSkipped) are preserved — only StepPending statuses are overwritten.
Navigation controls are auto-generated: Prev points to the previous step's Href (nil on the first step), Next points to the next step's Href (nil on the last step), and Submit is generated only on the final step.
type SwapMode ¶
type SwapMode string
SwapMode is a typed HTMX swap strategy for Control.Swap. These mirror the hx-swap attribute values. Defined as a separate type (rather than importing an htmx package) to keep linkwell dependency-free.
const ( SwapInnerHTML SwapMode = "innerHTML" SwapOuterHTML SwapMode = "outerHTML" SwapNone SwapMode = "none" SwapBeforeBegin SwapMode = "beforebegin" SwapAfterBegin SwapMode = "afterbegin" SwapBeforeEnd SwapMode = "beforeend" SwapAfterEnd SwapMode = "afterend" SwapDelete SwapMode = "delete" )
HTMX swap strategy constants.
type TabConfig ¶ added in v0.2.17
type TabConfig struct {
// ID is a unique identifier for this tab group (e.g., "user-tabs").
ID string
// Items is the ordered list of tab entries.
Items []TabItem
// Target is the shared default hx-target CSS selector for all tabs.
// Individual TabItem.Target values override this.
Target string
}
TabConfig holds the configuration for an in-page tabbed navigation component. The server decides which tabs exist and which is active — templates consume this to render the tab bar and content panel.
func NewTabConfig ¶ added in v0.2.17
NewTabConfig creates a TabConfig with the given ID, shared target selector, and tab items. Items with an empty Target inherit the shared target at render time.
type TabItem ¶ added in v0.2.17
type TabItem struct {
// Label is the user-visible text for this tab.
Label string
// Href is the content endpoint loaded when the tab is selected.
Href string
// Target is the hx-target CSS selector for this tab's content panel.
// Overrides TabConfig.Target when set.
Target string
// Icon is the icon name rendered alongside the label.
Icon Icon
// Active indicates this tab matches the current page. Set by SetActiveTab
// or manually by the handler.
Active bool
// Disabled renders the tab in a non-interactive state.
Disabled bool
// Badge is an optional count or status indicator displayed on the tab.
Badge string
// Swap is the HTMX swap strategy for this tab's content. Defaults to
// innerHTML when empty.
Swap SwapMode
}
TabItem is a server-computed tab entry for in-page tabbed navigation. Active state is determined by the handler (via SetActiveTab), not by client-side JavaScript. Each tab typically maps to an HTMX lazy-loaded content panel.
func SetActiveTab ¶ added in v0.2.17
SetActiveTab sets the Active flag on tab items using exact path matching. Returns a new slice; the input is not modified.
type TableCol ¶
type TableCol struct {
// Key identifies the column (maps to the "sort" query parameter value).
Key string
// Label is the visible column header text.
Label string
// SortDir is the current sort direction for this column.
SortDir SortDir
// SortURL is the HTMX request URL that toggles the sort direction. Built by
// SortableCol with the next direction pre-encoded.
SortURL string
// Target is the CSS selector for hx-target on the sort link.
Target string
// Include is the CSS selector for hx-include on the sort link (e.g.,
// "#filter-form" to forward filter state).
Include string
// Width is an optional CSS width hint (e.g., "120px", "20%").
Width string
// Sortable indicates whether the column header renders as a clickable sort link.
Sortable bool
}
TableCol is a pure-data column descriptor for a table header. Sort metadata (direction, toggle URL) is precomputed by the handler so templates render without logic. Non-sortable columns leave SortURL empty.
func SortableCol ¶
func SortableCol(key, label, currentSortKey, currentSortDir, baseURL, target, include string) TableCol
SortableCol creates a TableCol with precomputed sort state and toggle URL. Pass the current sort key and direction from the request's query parameters. The baseURL should have "sort" and "dir" params already stripped. Toggle logic: clicking an unsorted column sorts ascending, clicking the active ascending column toggles to descending, and clicking the active descending column toggles back to ascending.
Example ¶
package main
import (
"fmt"
"github.com/catgoose/linkwell"
)
func main() {
col := linkwell.SortableCol("name", "Name", "name", "asc", "/users", "#table", "#filter-form")
fmt.Printf("key=%s dir=%s sortable=%v\n", col.Key, col.SortDir, col.Sortable)
}
Output: key=name dir=asc sortable=true
type TableRowActionCfg ¶
type TableRowActionCfg struct {
EditURL string // GET URL to fetch the edit form for this row.
DeleteURL string // DELETE URL to remove this row.
RowTarget string // CSS selector for the row element.
TableTarget string // CSS selector for the table container.
ConfirmMsg string // hx-confirm message for the delete action.
ErrorTarget string // CSS selector for inline error display.
}
TableRowActionCfg configures Edit + Delete controls where Edit swaps the individual row (RowTarget) and Delete replaces the entire table (TableTarget). Use when deleting a row requires re-rendering the full table (e.g., to update row numbers or totals).
type Toast ¶ added in v0.2.16
type Toast struct {
// Message is the user-visible notification text.
Message string
// Variant determines the visual style (success, info, warning, error).
Variant ToastVariant
// Controls is an optional set of action affordances rendered with the toast
// (e.g., an "Undo" button).
Controls []Control
// AutoDismiss is the number of seconds before the toast auto-closes.
// Zero means the toast is sticky and must be dismissed manually.
AutoDismiss int
// OOBTarget is the CSS selector for an HTMX OOB swap target. When set,
// the toast renders as an out-of-band fragment.
OOBTarget string
// OOBSwap is the hx-swap-oob strategy (e.g., "afterbegin").
OOBSwap string
}
Toast is a pure-data descriptor for a transient notification. It is the success/info/warning complement to ErrorContext: the server decides what feedback to show and the template renders the appropriate alert with optional action controls. Toasts are value types; use the With* methods to derive modified copies.
func ErrorToast ¶ added in v0.2.16
ErrorToast creates a Toast with the error variant.
func SuccessToast ¶ added in v0.2.16
SuccessToast creates a Toast with the success variant.
func WarningToast ¶ added in v0.2.16
WarningToast creates a Toast with the warning variant.
func (Toast) WithAutoDismiss ¶ added in v0.2.16
WithAutoDismiss returns a copy of the Toast with the given auto-dismiss duration in seconds. Pass 0 for a sticky toast.
func (Toast) WithControls ¶ added in v0.2.16
WithControls returns a copy of the Toast with the given controls appended to the existing set. The original is not modified.
type ToastVariant ¶ added in v0.2.16
type ToastVariant string
ToastVariant identifies the severity or intent of a Toast notification. Templates map variants to visual styles (e.g., DaisyUI alert-success).
const ( // ToastSuccess indicates a successful operation. ToastSuccess ToastVariant = "success" // ToastInfo provides neutral informational feedback. ToastInfo ToastVariant = "info" // ToastWarning signals a non-blocking issue that deserves attention. ToastWarning ToastVariant = "warning" // ToastError indicates a failed operation (complement to ErrorContext). ToastError ToastVariant = "error" )

