steward

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 12, 2026 License: MIT Imports: 61 Imported by: 0

README

Steward

A server-rendered admin-panel framework for Go — a rewrite of the excellent dcat-admin (Laravel), built on Basecoat (shadcn/ui-style components on Tailwind CSS), HTMX, GORM, and Go generics.

Status: pre-release, under active development. APIs will change.

app, _ := steward.New(steward.Config{DB: db, SecretKey: key})

posts := steward.Register[Post](app).Title("Posts").Icon("news")
posts.Grid(func(g *steward.Grid[Post]) {
    g.Column("Title").Limit(40).Sortable()
    g.Column("Status").Badge(map[any]string{"draft": "secondary", "published": "green"})
    g.QuickSearch("Title", "Body")
})
posts.Form(func(f *steward.Form[Post]) {
    f.Text("Title").Rules("required|max:255")
    f.Markdown("Body")
})

ginsteward.Mount(router, app) // or mount app as a plain http.Handler

Highlights

  • Fluent, typed builders — Grid / Form / Detail declared in Go; callbacks receive your model type, never map[string]any.
  • Zero-config CRUD — steward.Register[User](app) alone yields a working resource; every field remains individually overridable.
  • RBAC + menu administration — roles, permissions (path matching), and a drag-and-drop menu manager, enforced by policies at resource, row, and field level.
  • Versioned migrations — embedded framework migrations plus a runner for your own; no silent AutoMigrate schema drift.
  • Headless-ready — every resource endpoint also serves JSON via Accept: application/json, with opt-in bearer tokens so API scripts and mobile clients authenticate without a cookie or a CSRF handshake.
  • Single binary, no Node anywhere — the UI bundle (Tailwind v4 + Basecoat + htmx) is compiled by the esbuild Go API and the Tailwind standalone binary (make assets), committed, and shipped via go:embed; override any template by dropping a file in your project.
  • Scaffolding CLI — steward new, steward make:resource (from a field spec, a live database, or a Go struct) with DB-type → field-type inference.

Features

Checked items work end-to-end today; unchecked items are on the roadmap.

Resources
  • Typed Grid[T] / Form[T] / Detail[T] builders on any GORM model
  • Zero-config CRUD — steward.Register[User](app) alone is a working resource
  • Repository[T] seam (GORM default; SQLite, MySQL, Postgres) with preloads and base scopes
  • Boot-time Verify() — every column reference checked at startup, not at click time
  • Headless JSON API on every resource endpoint (Accept: application/json) plus a _schema endpoint
  • Bearer-token auth for API and mobile clients (EnableTokenAuth), CSRF-exempt, inheriting the token owner's roles and policies; the token endpoint is rate-limited per username and per client IP
  • HTMX fragment navigation (SPA feel, server-rendered)
Grid
  • Sortable columns and display helpers (badge, bool, link, image, truncate, copyable, custom Display)
  • Quick-search DSL (field:value, >n, %contains%)
  • Filter panel (equals, like, greater/less, between, date range, select)
  • Filters and quick search across one-hop relations — Tags.Tag, Author.Name — via subqueries, so counts and pagination stay correct on has-many and many-to-many paths
  • Sorting by a relation column (needs a join; rejected at boot today)
  • Windowed pagination (1 … 18 19 20 … 37) with per-page selector
  • CSV export
  • Batch delete and custom row/batch/tool actions, confirmed via alert dialogs
  • Row actions as side-by-side buttons or a dropdown menu, panel-wide via Config.GridActions and per grid via Grid.ActionStyle
  • Inline editing (Column.Editable(), Column.Switch()) routed through form validation
  • Tree grids (Grid.Tree) and grouped column headers
  • Column show/hide picker (persisted per grid)
  • Drag-and-drop row reordering (Grid.Reorderable)
  • Row actions pinned to the trailing edge, so they stay reachable once a wide grid scrolls sideways
  • Fixed (pinned) columns generally
  • Quick-create row
  • Quick search backed by a Searcher (SQL LIKE today)
Form
  • 22 field kinds, including File/Image uploads via the Storage interface and a Richtext HTML editor whose input is allowlist-sanitized server-side
  • Declarative validation rules (required|max:255|unique:posts,title,{id}) with separate creation/update rules
  • Typed hooks — Submitted / Saving / Saved / Deleting / Deleted receive *T, never maps
  • BelongsTo searchable select and MultiSelect pivot sync
  • hasMany nested row forms (steward.HasMany[T,C], dcat protocol)
  • Fieldset and divider layout
  • Dirty-field-only updates; 422 inline errors in both HTML and JSON
  • Embeds (JSON-column nested forms)
  • File/Image/BelongsTo fields inside hasMany rows
  • Per-request conditional fields (Field.Show) — hidden fields are refused on submit and omitted from _schema, not merely hidden
  • Icon picker field over the full Lucide set (~1,600), collapsed by default and showing the current glyph; the grid draws from one cached sprite rather than inlining every icon, and Verify() reports a name set in code that does not resolve
  • Tabbed form layout
Detail
  • Field renderers (badge, bool, image, link, HTML, custom As)
  • Embedded relation grids (steward.RelationGrid[T,C])
Dashboard & widgets
  • Widget templates — card, metric (KPI), alert, and lazy (HTMX load-after-paint)
  • Dashboard builder — widgets and their column span declared in Go, each with a typed data callback; a failing widget reports in place instead of blanking the page
  • Widgets fetched individually via Lazy(), so one slow aggregate never blocks the page; an empty result set reports "no data" rather than a fault
  • Charts via Basecoat's Chart component (bar, line, pie, doughnut, radar, stacked) — themed by --chart-N, typed column-oriented Go API, runtime served per page rather than bundled. make vendor-chart once
  • Aggregate helpers over Repository[T] — Count, Sum, GroupCount, PeriodCount, PeriodSum, chart-ready via AggRows.Chart; day/month/ year buckets on SQLite, MySQL, and Postgres

Page composition stays in templates rather than a Go Row/Column/Layout object graph like dcat-admin's: Tailwind plus the template overlay already cover it, and an HTML DSL in Go would be more surface for less flexibility.

Auth, RBAC & administration
  • Encrypted cookie sessions, CSRF protection, bcrypt passwords
  • TOTP two-factor authentication — self-service enrolment with an in-process QR code, single-use recovery codes, replay-proof codes, and an optional panel-wide Require2FA
  • Config.LoginCheck to refuse an account (suspended, not yet activated)
  • Password reset flow via the SMTP Mailer
  • Roles and permissions with dcat-compatible HTTP path matching
  • Policy[T] per action plus RowScoper row-level scoping; menu visibility derives from policies
  • Menu administration — drag-and-drop tree, sync-from-code
  • Operation log (passwords masked) and settings key-value store
  • Profile page and admin:create-user
  • Permission definitions synced from registered resources (the menu-sync pattern: code owns the canonical entries, roles are granted in the DB; hand-written path rules stay as the escape hatch)
Platform & tooling
  • Versioned migrations (batches, up/down/status) — no silent AutoMigrate drift
  • steward CLI — new, make:resource (from a field spec, a live database, or a Go struct, with type inference), make:migration, publish
  • App runtime commands — serve, worker, migrate, menu:sync, admin:create-user
  • Cron scheduler (@every 10m, @daily, five-field cron) running in a separate worker process, deployable independently of the panel
  • Background job queue (enqueue from the panel, process in the worker)
  • Cache (in-memory built in, Redis in contrib/), Storage (local built in, S3 in contrib/), SMTP Mailer
  • Searcher interface with an in-memory full-text implementation
  • Template overlay (override any view by dropping a file), every Lucide icon embedded as one sprite, dark mode, no_ui build tag
  • Node-free asset pipeline — esbuild Go API + Tailwind standalone binary
  • Mount under Gin (contrib/ginsteward) or any http.Handler router

Development

make build   # build library + example
make test    # run tests
make lint    # golangci-lint
make run     # run the example app (SQLite, http://localhost:8321)

The example app seeds a default panel account, admin / admin — change it immediately on any instance that isn't a local sandbox (or create your own with go run . admin:create-user and delete the seeded one).

License

MIT — see LICENSE. Vendored frontend assets keep their own licenses: Basecoat (MIT), htmx (0BSD), Lucide (ISC).

Documentation

Overview

Package steward is a server-rendered admin-panel framework for Go — a rewrite of the Laravel dcat-admin package.

Steward gives an application a full admin panel from fluent, typed resource builders:

app, _ := steward.New(steward.Config{DB: db, SecretKey: key})
posts := steward.Register[Post](app).Title("Posts").Icon("news")
posts.Grid(func(g *steward.Grid[Post]) {
	g.Column("Title").Sortable()
})
ginsteward.Mount(router, app)

The core is a plain http.Handler; contrib/ginsteward provides a one-line Gin mount. Pages render server-side with Basecoat and HTMX, and every resource endpoint also serves JSON via Accept-header negotiation.

Source layout

Admin, Context, Resource[T], Grid[T], Form[T], and Detail[T] all refer to one another, so splitting them across packages would only buy import cycles. They stay one package and the file names carry the grouping:

admin, context, routes, middleware, render, cli   the panel and its runtime
resource, fieldtable                              registration, model schema
grid, grid_render, grid_actions                   the listing
form, form_render, form_nested                    create and edit
detail                                            the show page
dashboard, dashboard_chart, dashboard_aggregate   widgets
auth, auth_token, auth_twofactor                  signing in
rbac, rbac_policy, models, menu, menu_sync        accounts and authorization
repository, repository_gorm, repository_relation  data access

Pieces that need nothing from the panel live under internal/ instead, where the compiler holds them to it: htmlsafe (allowlist HTML sanitizing), rules (field validation), ratelimit, cron, qr, session, httpmatch, quickdsl.

Index

Constants

View Source
const (
	ExportPending = "pending"
	ExportRunning = "running"
	ExportDone    = "done"
	ExportFailed  = "failed"
)

Export status values.

View Source
const DefaultDiskName = "local"

DefaultDiskName is the disk an upload goes to when nothing names one.

View Source
const RoleAdministrator = "administrator"

RoleAdministrator is the slug of the seeded super-user role.

Variables

View Source
var ErrNotSigned = errors.New("storage: cannot sign this name")

ErrNotSigned reports that a name could not be signed.

View Source
var ErrUnsafePath = errors.New("storage: unsafe path")

ErrUnsafePath rejects names that escape the storage root.

Functions

func AcceptAllowsForTest

func AcceptAllowsForTest(accept, ext string) bool

AcceptAllowsForTest exposes the upload accept check to the example's tests, which live in another module and cannot reach an unexported function.

func BuiltinAssets

func BuiltinAssets() fs.FS

BuiltinAssets exposes the embedded static assets.

func BuiltinTemplates

func BuiltinTemplates() fs.FS

BuiltinTemplates exposes the embedded template tree — the source for `steward publish views` and the reference for override paths.

func CLI

func CLI(app App)

CLI parses os.Args and runs one command:

serve                     start the admin (default)
worker                    run App.Jobs on the scheduler (no HTTP)
migrate up                apply pending migrations
migrate down [-steps N]   roll back (default: last batch)
migrate status            list migrations
menu:sync                 re-sync menu entries from registered resources
search:reindex [-batch]   rebuild the search index from the database
admin:create-user         create a panel account interactively

func Count

func Count[T any](c *Context, conds ...Cond) (int64, error)

Count returns how many T rows match conds.

func HasMany

func HasMany[T any, C any](f *Form[T], relation string, fkPath string, fn func(*Form[C]))

HasMany embeds a repeatable child form inside T's form — dcat-admin's hasMany. relation names T's []C slice field (used to verify the association); fkPath names C's foreign-key field, set to the parent's key on save. Child rows follow dcat's protocol: existing rows are keyed by their id, new rows by "new_*", and rows flagged "_remove" are deleted.

steward.HasMany(f, "Items", "OrderID", func(cf *steward.Form[Item]) {
    cf.Text("Name").Rules("required")
    cf.Number("Qty")
})

v1 limits child fields to non-upload, non-relation kinds (no File/Image/ BelongsTo inside rows); Verify reports violations at boot.

func Preview

func Preview(href string, inner template.HTML) template.HTML

Preview wraps markup in the trigger that opens href in the panel's viewer — images and PDFs inline, anything else as a link. Image columns and detail fields do this already; this is for a cell built by hand.

g.ColumnFunc("photo", "Photo", func(r *Row) template.HTML {
    url := app.DiskURL("", r.Photo)
    return steward.Preview(url, template.HTML(`<img src="`+url+`" width="60"/>`))
})

func RelationGrid

func RelationGrid[T, C any](d *Detail[T], title string, bind func(q *ListQuery, m *T))

RelationGrid embeds a table of related C rows on T's detail page. C must be a registered resource; bind scopes the query to the shown record:

steward.RelationGrid(d, "Posts by this author",
    func(q *steward.ListQuery, a *Author) {
        q.Conds = append(q.Conds, steward.Cond{Path: "AuthorID", Op: steward.OpEq, Val: a.ID})
    })

func ServeMux

func ServeMux(a *Admin) *http.ServeMux

ServeMux is the routing `serve` puts in front of a panel: the panel itself, and — when it is mounted under a prefix — a redirect from the root to it.

At the root the panel is the whole mux. The bare-prefix and catch-all patterns needed otherwise are then either a second registration of "/", which panics, or, built from an empty prefix, not valid patterns at all.

func Sum

func Sum[T any](c *Context, path string, conds ...Cond) (float64, error)

Sum totals a numeric field across matching T rows.

Types

type Action

type Action struct {
	// contains filtered or unexported fields
}

Action is a custom operation on a resource, rendered as a button and dispatched to POST {resource}/_action/{name}. Row actions receive the clicked row's id, batch actions the selection, tool actions no ids.

func NewAction

func NewAction(name, label string, handler func(c *Context, ids []string) (*Envelope, error)) *Action

NewAction builds an action; name must be a slug (letters, digits, - _). The handler's returned envelope drives the client (toast, refresh, redirect, download); returning nil means Success + Refresh.

func (*Action) Confirm

func (a *Action) Confirm(message string) *Action

Confirm asks before dispatching.

func (*Action) Danger

func (a *Action) Danger() *Action

Danger marks the action destructive: a red button, or a destructive item in the menu style.

func (*Action) Icon

func (a *Action) Icon(name string) *Action

Icon sets the button's icon, by Lucide name.

type Admin

type Admin struct {
	// contains filtered or unexported fields
}

Admin is the panel: a plain http.Handler serving everything under Config.Prefix. Register resources against it, then mount it (ginsteward. Mount or http.Handle) — the first request triggers Build automatically.

func New

func New(cfg Config) (*Admin, error)

New validates the config and returns an unbuilt Admin. Resource registration happens between New and Build.

func (*Admin) Build

func (a *Admin) Build() error

Build freezes registration: wires join tables, runs framework migrations (unless disabled), compiles resources, parses templates, and constructs the route table. Calling it more than once is a no-op returning the first result; ServeHTTP calls it lazily.

func (*Admin) CommandSource

func (a *Admin) CommandSource(name string, fn CommandSource) *Admin

CommandSource adds a searchable section to the command palette.

app.CommandSource("Help", func(c *steward.Context, q string) []steward.CommandResult {
    return lookupDocs(q)
})

Resources that declare QuickSearch are searched already and need no source of their own.

func (*Admin) DB

func (a *Admin) DB() *gorm.DB

DB returns the underlying GORM handle.

func (*Admin) Dashboard

func (a *Admin) Dashboard(fn func(*Dashboard)) *Admin

Dashboard replaces the default home page with widgets declared in Go:

app.Dashboard(func(d *steward.Dashboard) {
    d.Metric("Users", countUsers).Span(1).Hint("all time")
    d.Template("Recent", "widgets/recent.html", recentRows).Span(2).Lazy()
})

Call it before Build. Without it the built-in overview page is served.

func (*Admin) DeleteNotification

func (a *Admin) DeleteNotification(ctx context.Context, userID, id uint) error

DeleteNotification removes one of an account's notifications.

func (*Admin) Disk

func (a *Admin) Disk(name string) (Disk, bool)

Disk returns a named disk. The second result is false for a name that was never configured.

func (*Admin) DiskNames

func (a *Admin) DiskNames() []string

DiskNames lists the configured disks, sorted.

func (*Admin) DiskURL

func (a *Admin) DiskURL(disk, name string) string

DiskURL turns a stored path into a URL on a named disk. A private disk gets a signed, expiring link; a public one gets the plain URL, which is the point of calling it public.

func (*Admin) Exports

func (a *Admin) Exports(ctx context.Context, userID uint, limit int) ([]ExportJob, error)

Exports returns an account's export jobs, newest first.

func (*Admin) Icons

func (a *Admin) Icons() []string

Icons lists the icon names available to this panel, sorted — for a form field or a custom page that lets someone choose one.

func (*Admin) MarkNotificationRead

func (a *Admin) MarkNotificationRead(ctx context.Context, userID, id uint) error

MarkNotificationRead marks one notification read. The user ID is part of the statement, so one account cannot mark another's.

func (*Admin) MarkNotificationsRead

func (a *Admin) MarkNotificationsRead(ctx context.Context, userID uint) error

MarkNotificationsRead marks every unread notification of an account read.

func (*Admin) MigrationRunner

func (a *Admin) MigrationRunner(app []migrate.Migration) *migrate.Runner

MigrationRunner returns a runner with the framework's core migrations registered plus any app migrations supplied. Used by Build (AutoMigrate) and by the app-side CLI.

func (*Admin) Notifications

func (a *Admin) Notifications(ctx context.Context, userID uint, limit int) ([]Notification, error)

Notifications returns an account's most recent notifications, unread first.

func (*Admin) Notify

func (a *Admin) Notify(ctx context.Context, userID uint, n Notification) error

Notify stores a notification for one account.

It writes a row and returns; nothing is delivered out of process, so a caller in a request handler pays one INSERT. ID, UserID and CreatedAt are set by this call and need not be filled in.

func (*Admin) NotifyRole

func (a *Admin) NotifyRole(ctx context.Context, n Notification, roleSlugs ...string) error

NotifyRole stores a notification for every account holding any of the given roles. Accounts holding two of them are notified once.

The administrator role is not implicit here: it short-circuits permission checks, not delivery, so notifying "editor" does not reach an administrator who is not one.

func (*Admin) NotifyUsers

func (a *Admin) NotifyUsers(ctx context.Context, userIDs []uint, n Notification) error

NotifyUsers stores the same notification for several accounts in one statement. Duplicate and zero IDs are dropped.

func (*Admin) Prefix

func (a *Admin) Prefix() string

Prefix returns the mount path ("/admin").

func (*Admin) PruneExports

func (a *Admin) PruneExports(ctx context.Context, age time.Duration) (int64, error)

PruneExports deletes finished jobs older than age, and the files they point at. Nothing calls it for you.

func (*Admin) PruneNotifications

func (a *Admin) PruneNotifications(ctx context.Context, age time.Duration) (int64, error)

PruneNotifications deletes read notifications older than age, and returns how many went. Unread ones are always kept, however old.

Nothing calls this for you: the table grows until something does. Run it from a cron entry or the app's own scheduler.

func (*Admin) Reindex

func (a *Admin) Reindex(ctx context.Context, batch int) (map[string]int, error)

Reindex rebuilds every searchable resource's documents. It reports what it wrote per resource, so a backfill that skipped something says so.

func (*Admin) RunPendingExports

func (a *Admin) RunPendingExports(ctx context.Context) (int, error)

RunPendingExports builds every queued export, oldest first, and returns how many it completed.

The panel runs this itself unless DisableExportWorker is set; call it from a worker's scheduler instead when the panel should not do the work. Claiming is a conditional update, so several processes may run it at once without two of them building the same file.

func (*Admin) ServeHTTP

func (a *Admin) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements http.Handler; the panel behaves identically however it is mounted (net/http, Gin via WrapH, chi, etc.).

func (*Admin) SetSetting

func (a *Admin) SetSetting(ctx context.Context, slug, value string) error

SetSetting upserts a KV row and refreshes the cache.

func (*Admin) Setting

func (a *Admin) Setting(ctx context.Context, slug string) (string, error)

Setting returns the stored value for slug ("" when absent), cached.

func (*Admin) StorageURL

func (a *Admin) StorageURL(name string) string

StorageURL turns a stored value into something a browser can fetch.

File and Image form fields store a storage-relative path, not a URL, so a column or field that puts one straight into an href or a src produces a reference the browser resolves against the current page. Anything already absolute is returned untouched, since an app may store full URLs instead.

Use this inside a Display or Link function, where the URL is yours to compute:

g.Column("File").Link(func(m *Magazine) string { return app.StorageURL(m.File) })

func (*Admin) StorageURLOn

func (a *Admin) StorageURLOn(disk, name string) string

StorageURLOn is StorageURL against a named disk.

func (*Admin) UnreadNotifications

func (a *Admin) UnreadNotifications(ctx context.Context, userID uint) (int64, error)

UnreadNotifications counts an account's unread notifications.

func (*Admin) Verify

func (a *Admin) Verify() error

Verify runs Build and returns every configuration error collected during resource compilation, joined. Assert it in a test to catch bad column references at CI time instead of request time.

type AdminToken

type AdminToken struct {
	ID         uint   `gorm:"primaryKey"`
	UserID     uint   `gorm:"index;not null"`
	Name       string `gorm:"size:120"` // client label: "iPhone", "CI deploy"
	Hash       string `gorm:"size:64;uniqueIndex"`
	LastUsedAt *time.Time
	ExpiresAt  *time.Time `gorm:"index"`
	CreatedAt  time.Time
}

AdminToken is a bearer credential for API and mobile clients, belonging to an AdminUser and inheriting that user's roles, permissions, and policies.

Hash holds a SHA-256 of the token, not a bcrypt digest: tokens carry 256 bits of entropy, so a fast hash is sound and — unlike bcrypt — lets lookup be a single indexed query instead of a scan over every row.

func (AdminToken) TableName

func (AdminToken) TableName() string

type AdminUser

type AdminUser struct {
	ID            uint    `gorm:"primaryKey"`
	Username      string  `gorm:"size:120;uniqueIndex"`
	Password      string  `gorm:"size:100"` // bcrypt hash
	Name          string  `gorm:"size:255"`
	Avatar        string  `gorm:"size:255"`
	Email         *string `gorm:"size:255;uniqueIndex"`
	RememberToken string  `gorm:"size:100"`
	CreatedAt     time.Time
	UpdatedAt     time.Time

	// Two-factor authentication (see auth_twofactor.go). Enrolment is complete
	// only once TwoFactorConfirmedAt is set, so a scanned-but-unverified
	// secret never locks anyone out. TwoFactorLastStep records the most
	// recently accepted time step, which is what makes a code single-use.
	TwoFactorSecret      string `gorm:"size:64"`
	TwoFactorConfirmedAt *time.Time
	TwoFactorRecovery    string `gorm:"type:text"` // newline-separated SHA-256 digests
	TwoFactorLastStep    int64  `gorm:"default:0"`

	Roles []Role `gorm:"many2many:admin_role_users;joinForeignKey:user_id;joinReferences:role_id"`
}

AdminUser is a panel account. Email is optional and only required for the password-reset flow.

func (*AdminUser) HasRole

func (u *AdminUser) HasRole(slugs ...string) bool

HasRole reports whether the user holds any of the given role slugs. Roles must be loaded (the auth middleware preloads them), so this costs no query.

It answers "who is this?", which belongs in a resource's Policy or a form field's Show predicate. It is not a substitute for permissions: those live in the database precisely so an operator can change them without a deploy.

func (*AdminUser) IsAdministrator

func (u *AdminUser) IsAdministrator() bool

IsAdministrator reports whether the user holds the built-in administrator role (seeded with ID 1), which short-circuits every permission check.

func (AdminUser) TableName

func (AdminUser) TableName() string

func (*AdminUser) TwoFactorEnabled

func (u *AdminUser) TwoFactorEnabled() bool

TwoFactorEnabled reports whether the account has completed enrolment.

type AggFunc

type AggFunc string

AggFunc is the aggregate to compute.

const (
	AggCount AggFunc = "count"
	AggSum   AggFunc = "sum"
	AggAvg   AggFunc = "avg"
	AggMin   AggFunc = "min"
	AggMax   AggFunc = "max"
)

type AggQuery

type AggQuery struct {
	// Func defaults to AggCount.
	Func AggFunc
	// Path is the field path to aggregate. Required except for AggCount.
	Path string
	// GroupBy is the field path to group by. Empty returns a single total.
	GroupBy string
	// Period buckets GroupBy by date instead of grouping on its exact value.
	Period Period
	// Conds filter the rows considered, using the same operators as a grid.
	Conds []Cond
	// Scopes are backend-specific refinements — func(*gorm.DB) *gorm.DB for
	// the GORM repository, matching ListQuery.Scopes.
	Scopes []any
	// Limit caps the number of groups returned. Zero means no cap.
	Limit int
	// Desc orders groups by value descending (top-N). Otherwise groups come
	// back ordered by key, which is chronological for a Period.
	Desc bool
}

AggQuery is one aggregate read.

type AggRow

type AggRow struct {
	Key   string
	Value float64
}

AggRow is one group's result.

type AggRows

type AggRows []AggRow

AggRows is an ordered aggregate result.

func Aggregate

func Aggregate[T any](c *Context, q *AggQuery) (AggRows, error)

Aggregate runs an arbitrary aggregate against T.

func GroupCount

func GroupCount[T any](c *Context, path string, limit int, conds ...Cond) (AggRows, error)

GroupCount counts T grouped by a field path, largest group first — the shape a pie or bar chart wants. Pass limit 0 for every group.

func PeriodCount

func PeriodCount[T any](c *Context, path string, p Period, conds ...Cond) (AggRows, error)

PeriodCount counts T bucketed by a date field, oldest bucket first.

Buckets with no rows are absent rather than zero: the query only sees rows that exist. Fill gaps yourself if a continuous axis matters.

func PeriodSum

func PeriodSum[T any](c *Context, sumPath, datePath string, p Period, conds ...Cond) (AggRows, error)

PeriodSum totals a numeric field bucketed by a date field, oldest first.

func (AggRows) Chart

func (rows AggRows) Chart(t ChartType, label string) *ChartData

Chart turns the rows into a single-series chart, keys becoming labels.

func (AggRows) Total

func (rows AggRows) Total() float64

Total sums every row's value.

type Aggregator

type Aggregator interface {
	Aggregate(ctx context.Context, q *AggQuery) (AggRows, error)
}

Aggregator is the optional repository capability behind the helpers below.

type AllowAll

type AllowAll[T any] struct{}

AllowAll is an embeddable Policy allowing everything; override selectively.

func (AllowAll[T]) Create

func (AllowAll[T]) Create(*Context) bool

Create implements Policy.

func (AllowAll[T]) Delete

func (AllowAll[T]) Delete(*Context, *T) bool

Delete implements Policy.

func (AllowAll[T]) Update

func (AllowAll[T]) Update(*Context, *T) bool

Update implements Policy.

func (AllowAll[T]) View

func (AllowAll[T]) View(*Context, *T) bool

View implements Policy.

func (AllowAll[T]) ViewAny

func (AllowAll[T]) ViewAny(*Context) bool

ViewAny implements Policy.

type App

type App struct {
	// Build constructs the configured Admin (required). It runs for every
	// command; keep it side-effect free beyond wiring.
	Build func() (*Admin, error)

	// Serve starts the HTTP server (optional). The default serves the
	// admin on Addr with net/http.
	Serve func(a *Admin) error

	// Addr is the default listen address for the built-in server (":8080").
	Addr string

	// Migrations are the app's own migrations, registered under "app".
	Migrations []migrate.Migration

	// Jobs registers recurring jobs on the scheduler. They run only in the
	// `worker` command — a separate process from `serve` — so the panel and
	// background work deploy, restart, and scale independently. Use a.DB()
	// for database access.
	Jobs func(a *Admin, s Scheduler) error
}

App wires an application into the standard runtime commands. Because migrations are Go code living in the app, runtime operations execute in the app binary — `go run . migrate up` — while the `steward` CLI handles code generation only.

type BadgeColor

type BadgeColor string

BadgeColor names a badge's palette entry.

badgeHTML renders a Basecoat badge; named colors map onto Tailwind palette utilities (kept in sync with the @source inline safelist in frontend/src/app.css), everything else falls back to the secondary variant. BadgeColor names one of the badge palettes. It is a string type rather than an integer enum on purpose: a value the framework does not know still compiles, so a panel with its own palette is not shut out — but the ones that exist are discoverable and a typo in them is caught by Verify.

const (
	BadgeGreen       BadgeColor = "green"
	BadgeBlue        BadgeColor = "blue"
	BadgeAzure       BadgeColor = "azure"
	BadgePurple      BadgeColor = "purple"
	BadgeOrange      BadgeColor = "orange"
	BadgeYellow      BadgeColor = "yellow"
	BadgeRed         BadgeColor = "red"
	BadgeDestructive BadgeColor = "destructive"
	BadgeOutline     BadgeColor = "outline"
	BadgeSecondary   BadgeColor = "secondary"
)

type Cache

type Cache interface {
	Get(ctx context.Context, key string) ([]byte, bool, error)
	Set(ctx context.Context, key string, val []byte, ttl time.Duration) error
	Delete(ctx context.Context, keys ...string) error
}

Cache is the small byte cache backing menu/settings lookups. The default is the in-process MemoryCache; supply a Redis-backed implementation for multi-instance deployments.

type ChartData

type ChartData struct {
	Type   ChartType
	Labels []string
	Series []ChartSeries
	// Legend draws Basecoat's generated legend beneath the canvas.
	Legend bool
	// Stacked stacks bar series on both axes.
	Stacked bool
}

ChartData is a chart's full description.

type ChartSeries

type ChartSeries struct {
	// Label names the series in the legend and tooltip.
	Label string
	// Values must be the same length as ChartData.Labels.
	Values []float64
	// Color is any CSS colour. Empty takes the next palette entry.
	Color string
	// Fill shades the area under a line series.
	Fill bool
	// Key overrides the payload key for this series. Empty derives one, which
	// is what you normally want.
	Key string
}

ChartSeries is one set of values plotted against ChartData.Labels.

type ChartType

type ChartType string

ChartType selects the Chart.js chart type.

const (
	ChartBar      ChartType = "bar"
	ChartLine     ChartType = "line"
	ChartPie      ChartType = "pie"
	ChartDoughnut ChartType = "doughnut"
	ChartRadar    ChartType = "radar"
)

type Column

type Column[T any] struct {
	// contains filtered or unexported fields
}

Column configures one grid column; every method returns the column for chaining. Display callbacks receive the typed row — never a map.

func (*Column[T]) Badge

func (c *Column[T]) Badge(colors map[any]BadgeColor) *Column[T]

Badge renders the value as a colored badge; keys are raw values (or their fmt representation), values are color names ("green", "blue", "azure", "purple", "orange", "yellow", "red", "secondary", "outline").

func (*Column[T]) Bool

func (c *Column[T]) Bool(labels ...string) *Column[T]

Bool renders truthy/falsy values as a status. It says Yes and No unless given two words of its own: Bool("Ya", "Tidak").

func (*Column[T]) Copyable

func (c *Column[T]) Copyable() *Column[T]

Copyable adds a copy-to-clipboard affordance.

func (*Column[T]) Disk

func (c *Column[T]) Disk(name string) *Column[T]

Disk names which storage disk this column's stored paths live on, for the helpers that resolve one into a URL. Unset, the default disk is used.

func (*Column[T]) Display

func (c *Column[T]) Display(fn func(v any, row *T) template.HTML) *Column[T]

Display renders the cell with a custom function (receives the raw field value and the typed row).

func (*Column[T]) Editable

func (c *Column[T]) Editable() *Column[T]

Editable renders a click-to-edit text cell saving through the form pipeline; the form must declare a field for the same path.

func (*Column[T]) Help

func (c *Column[T]) Help(s string) *Column[T]

Help adds a header tooltip.

func (*Column[T]) Hide

func (c *Column[T]) Hide() *Column[T]

Hide renders the column hidden (still exported).

func (*Column[T]) Image

func (c *Column[T]) Image(width, height int) *Column[T]

Image renders the value (a URL or storage path) as a thumbnail. A storage-relative path resolves through the configured Storage; see Admin.StorageURL.

func (*Column[T]) Limit

func (c *Column[T]) Limit(n int) *Column[T]

Limit truncates long text with an ellipsis and a title tooltip.

func (c *Column[T]) Link(href func(row *T) string) *Column[T]

Link renders the value as an anchor; href receives the typed row.

func (*Column[T]) Sortable

func (c *Column[T]) Sortable() *Column[T]

Sortable makes the header clickable.

func (*Column[T]) Switch

func (c *Column[T]) Switch() *Column[T]

Switch renders a live toggle that saves immediately through the form pipeline. The resource's form must declare a Switch field for the same path — its rules and hooks apply to inline edits too.

func (*Column[T]) Using

func (c *Column[T]) Using(m map[any]string) *Column[T]

Using maps raw values to replacement text ({"1": "Yes"}).

func (*Column[T]) Width

func (c *Column[T]) Width(px int) *Column[T]

Width fixes the column width in pixels.

type CommandResult

type CommandResult struct {
	// Group heads the section it appears under — a resource's title, or
	// whatever a CommandSource calls itself.
	Group string `json:"group"`
	// Title is what the reader reads; Subtitle is the dimmer line beside it.
	Title    string `json:"title"`
	Subtitle string `json:"subtitle,omitempty"`
	// URL is where choosing it goes.
	URL string `json:"url"`
	// Icon is a Lucide name, blank for none.
	Icon string `json:"icon,omitempty"`
}

CommandResult is one row the command palette offers.

type CommandSource

type CommandSource func(c *Context, query string) []CommandResult

CommandSource answers the palette for one kind of thing. Register it with Admin.CommandSource; it runs on every keystroke past the minimum length, so it should be a bounded query rather than a scan.

type Cond

type Cond struct {
	Path string
	Op   Op
	Val  any
	Val2 any
}

Cond is one filter condition against a field path ("Title", "Status"). Val2 is the upper bound for OpBetween.

type Config

type Config struct {
	DB *gorm.DB

	// Prefix is the URL the panel mounts under (default "/admin").
	Prefix string

	// Brand names the panel in the sidebar and titles (default "Steward").
	Brand string

	// CurrencySymbol prefixes every Currency field (default "$"). A single
	// field overrides it with Field.Symbol.
	CurrencySymbol string

	// SignedURLTTL is how long a link to a stored file stays good
	// (default 15 minutes).
	SignedURLTTL time.Duration

	// PublicUploads makes the default disk public. Prefer naming a disk in
	// Disks and setting Disk.Public, which lets one panel keep both kinds.
	PublicUploads bool

	// Disks are the named places files can be stored, each public or private.
	// A File or Image field picks one with Field.Disk; without Disks a panel
	// has exactly one, named by DefaultDisk, backed by Storage.
	//
	//	Disks: map[string]steward.Disk{
	//	    "public":  {Public: true},                 // local, under UploadDir/public
	//	    "private": {},                             // local, gated and signed
	//	    "media":   {Storage: s3, Public: false},   // presigned S3
	//	}
	Disks map[string]Disk

	// ExportDisk is where finished background exports are written. Empty means
	// DefaultDisk, which is usually right — but not when the default disk's
	// directory is served by something other than the panel, since an export
	// carries whatever rows its owner could read.
	ExportDisk string

	// DefaultDisk is where an upload goes when its field names no disk
	// (default "local").
	DefaultDisk string

	// TablePrefix names the framework tables (default "admin_"). It is
	// process-global; two Admins with different prefixes in one process are
	// not supported.
	TablePrefix string

	// SecretKey signs and encrypts sessions, CSRF, and remember tokens.
	// Changing it invalidates all sessions. Minimum 16 bytes.
	SecretKey []byte

	// BackgroundExportRows is the match size past which a whole-table export
	// becomes a job rather than a download. Zero means the default (10,000);
	// negative always streams, whatever the size.
	BackgroundExportRows int

	// DisableExportWorker stops the panel process from building queued
	// exports. Something else then has to call RunPendingExports — a worker's
	// scheduler — or nothing does and they stay pending.
	DisableExportWorker bool

	// DisableNotifications hides the bell in the header and unmounts its
	// endpoints. The table is still created, so turning it back on later
	// needs no migration.
	DisableNotifications bool

	// DisableAutoMigrate skips running the embedded framework migrations at
	// Build. Recommended in production: run them explicitly via the app's
	// `migrate up` command instead.
	DisableAutoMigrate bool

	// Dev re-parses templates on every request and serves assets uncached.
	Dev bool

	// TemplatesFS overlays the embedded templates; files here win. Paths
	// mirror the embedded tree, e.g. "layout/sidebar.html".
	TemplatesFS fs.FS

	// AssetsFS overlays the embedded static assets (e.g. extra icons under
	// "icons/name.svg").
	AssetsFS fs.FS

	// UploadDir is LocalStorage's root when no Storage is supplied
	// (default "./uploads").
	UploadDir string

	Cache   Cache   // default: in-process MemoryCache
	Storage Storage // default: LocalStorage at UploadDir

	// Searcher backs quick search and the command palette for resources that
	// declared Searchable. Without one they fall back to SQL LIKE.
	Searcher Searcher
	Mailer   Mailer // optional; enables password reset

	// AuthExcept lists extra path patterns (relative to Prefix, * globs)
	// that skip authentication and permission checks.
	AuthExcept []string

	// GridActions chooses how every grid presents a row's actions:
	// GridActionsButtons (the default) lays them side by side, GridActionsMenu
	// collapses them behind one trigger. A single grid can differ via
	// Grid.ActionStyle.
	GridActions GridActionStyle

	// FilterLayout chooses where every grid's filter panel lives:
	// FiltersAbove (the default) opens it in place between the toolbar and the
	// rows, FiltersDrawer opens it over the page from the right. A single grid
	// can differ via Grid.FilterLayout.
	FilterLayout GridFilterLayout

	// Require2FA makes TOTP two-factor authentication mandatory: an account
	// that has not enrolled is redirected to its profile page and can reach
	// nothing else until it does. Off by default, in which case each user
	// decides for themselves from the same page.
	//
	// Bearer-token clients are exempt — they hold an explicit credential that
	// is already separately scoped, and have no session to enrol through.
	Require2FA bool

	// LoginCheck runs after the password (and second factor, if any) has been
	// accepted and before the session is issued. A returned error refuses the
	// login and its message is shown on the form, so it is the seam for
	// application-level account state: "suspended", "not yet activated",
	// "outside permitted hours".
	//
	// It cannot be used to *grant* a login, only to withhold one.
	LoginCheck func(ctx context.Context, u *AdminUser) error

	// EnableTokenAuth accepts "Authorization: Bearer <token>" alongside the
	// session cookie, and mounts POST/DELETE {Prefix}/auth/token so API and
	// mobile clients can mint and revoke their own credentials.
	//
	// Off by default: enabling it exposes a credential-issuing endpoint that
	// takes a username and password, so it should be a deliberate choice
	// rather than something an upgrade turns on. Tokens inherit their user's
	// roles, permissions, and policies — scope an API client by giving it its
	// own AdminUser with a restricted role, not the administrator account.
	EnableTokenAuth bool

	// TokenTTL bounds how long an issued token stays valid. Zero means the
	// default of 30 days; a negative duration means tokens never expire.
	TokenTTL time.Duration

	// TokenRateLimit caps attempts on {Prefix}/auth/token within
	// TokenRateWindow, counted per username. Client IPs are capped in the same
	// window at six times this figure — looser, because a proxy collapses many
	// clients onto one address, so the per-username bound is what really
	// protects an account. Successful and failed attempts both count.
	//
	// Zero means 5 per window; a negative value disables limiting. Limits are
	// per process, so N replicas admit N times the rate.
	TokenRateLimit int

	// TokenRateWindow is the rate-limit window. Zero means one minute.
	TokenRateWindow time.Duration

	Logger *slog.Logger
}

Config configures one Admin. DB and SecretKey are required; everything else has a working default.

type Context

type Context struct {
	W     http.ResponseWriter
	R     *http.Request
	Admin *Admin

	// User is the authenticated account, nil on public routes (login).
	User *AdminUser
	// contains filtered or unexported fields
}

Context wraps one admin request. Handlers receive it instead of the raw (w, r) pair and return an error; the router renders returned errors as the error page or an error envelope depending on the client.

func (*Context) CSRF

func (c *Context) CSRF() string

CSRF returns the session's CSRF token (issued by the session middleware).

func (*Context) Ctx

func (c *Context) Ctx() context.Context

Ctx returns the request's context.Context for repository calls.

func (*Context) Envelope

func (c *Context) Envelope(e *Envelope) error

Envelope writes the unified mutation response.

func (*Context) Flash

func (c *Context) Flash(typ, msg string)

Flash queues a one-shot message shown on the next rendered page. It persists the session cookie immediately, so call it before writing a body.

func (*Context) JSON

func (c *Context) JSON(status int, v any) error

JSON writes v with the given status.

func (*Context) Layout

func (c *Context) Layout(title string, nodes ...Node) error

Layout renders a tree of rows and columns as a page, with the panel's chrome around it.

return c.Layout("Reports",
    steward.Row(
        steward.Col(8, steward.Card("Trend", steward.Chart(data))),
        steward.Col(4, steward.Metric("This year", 1752, "published")),
    ),
)

func (*Context) Redirect

func (c *Context) Redirect(url string) error

Redirect sends a client-appropriate redirect: HX-Redirect header for HTMX requests (full navigation client-side), 302 otherwise.

func (*Context) Render

func (c *Context) Render(name, title string, data any) error

Render writes a template inside the admin layout — the seam for custom Resource.Page handlers. name is a full relative template path resolved through the overlay FS ("pages/stats.html"); data lands in .Data.

func (*Context) URL

func (c *Context) URL(parts ...string) string

URL joins path segments onto the admin prefix ("/admin", "auth/login" → "/admin/auth/login").

func (*Context) WantsFragment

func (c *Context) WantsFragment() bool

WantsFragment reports an HTMX partial-navigation request: the response should be the page content only, not the full layout. Boosted requests are full-page swaps and still want the fragment (layout stays put).

func (*Context) WantsJSON

func (c *Context) WantsJSON() bool

WantsJSON reports Accept-header preference for JSON (the headless API) or an XMLHttpRequest-style client.

type Dashboard

type Dashboard struct {
	// contains filtered or unexported fields
}

Dashboard collects the widgets shown on the panel's home page.

func (*Dashboard) Chart

func (d *Dashboard) Chart(title string, load func(*Context) (*ChartData, error)) *Widget

Chart adds a chart tile drawn by Basecoat's Chart component. load returns the series to plot; see ChartData. Defaults to spanning two columns, since a chart squeezed into one is rarely readable.

Requires the chart assets: run `make vendor-chart` once, then `make assets`. Without them the tile explains itself rather than rendering blank.

func (*Dashboard) Metric

func (d *Dashboard) Metric(label string, load func(*Context) (any, error)) *Widget

Metric adds a KPI tile. load runs per request and its result is stringified by the template, so returning an int, string, or fmt.Stringer all work.

func (*Dashboard) Row

func (d *Dashboard) Row(cols ...Node) *Dashboard

Row arranges tiles explicitly rather than letting them flow into the dashboard's three-column grid, and is the same Row a custom page uses:

app.Dashboard(func(d *steward.Dashboard) {
    d.Row(
        steward.Col(8, d.Chart("Trend", trend)),
        steward.Col(4,
            d.Metric("This year", countThisYear),
            d.Metric("This month", countThisMonth),
        ),
    )
})

A tile placed in a row is not also flowed into the grid.

func (*Dashboard) Template

func (d *Dashboard) Template(title, tmpl string, load func(*Context) (any, error)) *Widget

Template adds a tile rendered from a template of your own, receiving whatever load returns as its data. Pass a nil load for a static tile.

type Detail

type Detail[T any] struct {
	// contains filtered or unexported fields
}

Detail configures a resource's show view. Without configuration every direct field renders with its type default.

func (*Detail[T]) Field

func (d *Detail[T]) Field(path string, label ...string) *DetailField[T]

Field adds one field row to the detail panel.

func (*Detail[T]) FieldFunc

func (d *Detail[T]) FieldFunc(name, label string, fn func(row *T) template.HTML) *DetailField[T]

FieldFunc adds a row whose value is computed from the whole record rather than read from one path, for anything a struct field cannot name: a collection, a summary, several values at once.

type DetailField

type DetailField[T any] struct {
	// contains filtered or unexported fields
}

DetailField is one show-view row; transformer methods chain.

func (*DetailField[T]) As

func (df *DetailField[T]) As(fn func(v any, m *T) template.HTML) *DetailField[T]

As renders the value with a custom function.

func (*DetailField[T]) Badge

func (df *DetailField[T]) Badge(colors map[any]BadgeColor) *DetailField[T]

Badge renders a colored badge (see Column.Badge). With Using, the colour is keyed on the stored value and the text comes from Using's map.

func (*DetailField[T]) Block

func (df *DetailField[T]) Block() *DetailField[T]

Block puts the value under its label across the row's full width, rather than beside it. HTML and Markdown do this already.

func (*DetailField[T]) Bool

func (df *DetailField[T]) Bool(labels ...string) *DetailField[T]

Bool renders a truthy/falsy value as a status. It says Yes and No unless given two words of its own: Bool("Ya", "Tidak").

func (*DetailField[T]) Copyable

func (df *DetailField[T]) Copyable() *DetailField[T]

Copyable adds a button that copies the value to the clipboard. It copies what is stored, not what is displayed, so a formatted number or a shortened path still yields the value someone would paste elsewhere.

func (*DetailField[T]) Disk

func (df *DetailField[T]) Disk(name string) *DetailField[T]

Disk names which storage disk this field's stored paths live on, for the helpers that resolve one into a URL. Unset, the default disk is used.

func (*DetailField[T]) Filesize

func (df *DetailField[T]) Filesize() *DetailField[T]

Filesize renders a byte count as a human size.

func (*DetailField[T]) HTML

func (df *DetailField[T]) HTML() *DetailField[T]

HTML renders the value as markup rather than escaped text — the read side of a Form.Richtext field.

The value is sanitized again here, not merely trusted for having been sanitized on save. Rows predating the Richtext field, rows written by a migration or a direct SQL fix, and rows from another writer never passed through that path, so cleaning on render is what makes the guarantee hold for the data actually in the table.

func (*DetailField[T]) Image

func (df *DetailField[T]) Image(width, height int) *DetailField[T]

Image renders the value (URL or storage path) as an image; storage paths resolve through the configured Storage at render time.

func (*DetailField[T]) JSON

func (df *DetailField[T]) JSON() *DetailField[T]

JSON pretty-prints the value as a code block.

func (df *DetailField[T]) Link() *DetailField[T]

Link renders the value as a link to itself, for a column holding a URL or an uploaded file's path. A storage-relative path resolves through the configured Storage, so a File field's value downloads rather than 404ing; an absolute URL is left as it stands.

func (*DetailField[T]) Markdown

func (df *DetailField[T]) Markdown() *DetailField[T]

Markdown renders the value as markdown (GitHub flavour), sanitized through the same allowlist a Richtext value passes.

func (*DetailField[T]) Preformatted

func (df *DetailField[T]) Preformatted() *DetailField[T]

Preformatted renders the value as text, keeping its line breaks and runs of spaces. This is what Markdown did before it rendered anything.

func (*DetailField[T]) Using

func (df *DetailField[T]) Using(m map[any]string) *DetailField[T]

Using maps stored values to display text.

type Disk

type Disk struct {
	// Storage is the backend. A LocalStorage left without a Dir is filled in
	// from the disk's name under Config.UploadDir.
	Storage Storage

	// Public serves this disk's files to anyone who asks, and its URLs are
	// plain and permanent. A private disk — the default — is served only to a
	// session or a signed URL, and StorageURL signs for it.
	Public bool
}

Disk is one named place files are stored. Naming several lets an upload go where it belongs — a public disk for images a website embeds, a private one for documents only the panel should hand out.

type Envelope

type Envelope struct {
	Status bool                `json:"status"`
	Data   *EnvelopeData       `json:"data,omitempty"`
	HTML   string              `json:"html,omitempty"`
	Errors map[string][]string `json:"errors,omitempty"`
	// contains filtered or unexported fields
}

Envelope is the unified JSON response for every mutation and AJAX interaction, ported verbatim from dcat-admin so client behavior is uniform: admin.js interprets Data.Then to redirect, refresh, download, or run script after showing the toast.

func Error

func Error(msg string) *Envelope

Error builds an error envelope.

func Info

func Info(msg string) *Envelope

Info builds an info envelope.

func Success

func Success(msg string) *Envelope

Success builds a success envelope.

func ValidationErrors

func ValidationErrors(errs map[string][]string) *Envelope

ValidationErrors builds the HTTP 422 envelope carrying per-field messages.

func Warning

func Warning(msg string) *Envelope

Warning builds a warning envelope.

func (*Envelope) Alert

func (e *Envelope) Alert() *Envelope

Alert renders a blocking dialog instead of a toast.

func (*Envelope) Code

func (e *Envelope) Code(status int) *Envelope

Code overrides the HTTP status (default 200, or 422 for ValidationErrors).

func (*Envelope) Detail

func (e *Envelope) Detail(s string) *Envelope

Detail adds secondary toast text.

func (*Envelope) Download

func (e *Envelope) Download(url string) *Envelope

Download triggers a file download.

func (*Envelope) Location

func (e *Envelope) Location(url string) *Envelope

Location performs a full browser navigation.

func (*Envelope) Redirect

func (e *Envelope) Redirect(url string) *Envelope

Redirect navigates via the HTMX-aware client router after the toast.

func (*Envelope) Refresh

func (e *Envelope) Refresh() *Envelope

Refresh reloads the current view.

func (*Envelope) Script

func (e *Envelope) Script(js string) *Envelope

Script runs a JS snippet (trusted, author-supplied) after the toast.

type EnvelopeData

type EnvelopeData struct {
	Message string `json:"message,omitempty"`
	Type    string `json:"type,omitempty"` // success | error | warning | info
	Alert   bool   `json:"alert,omitempty"`
	Detail  string `json:"detail,omitempty"`
	Timeout int    `json:"timeout,omitempty"` // seconds; 0 = client default
	Then    *Then  `json:"then,omitempty"`
}

EnvelopeData carries the toast and follow-up action.

type ExportJob

type ExportJob struct {
	ID     uint   `gorm:"primaryKey"`
	UserID uint   `gorm:"not null;index"`
	Slug   string `gorm:"size:120;not null"`

	// Query is the grid's own query string, so the export covers exactly what
	// the reader was looking at: filters, quick search, and sort.
	Query string `gorm:"type:text"`

	// Status is one of pending, running, done, failed. Indexed because the
	// runner's only question is "is there a pending one".
	Status string `gorm:"size:16;not null;index"`

	Rows  int64 `gorm:"column:row_count"`
	Bytes int64
	Disk  string `gorm:"size:60"`
	Path  string `gorm:"size:512"`
	Err   string `gorm:"type:text"`

	CreatedAt time.Time
	StartedAt *time.Time
	DoneAt    *time.Time
}

ExportJob is one requested CSV, built away from the request that asked for it. A whole-table export of any size holds a connection open for as long as it takes to write, and every proxy in front of the panel has an opinion about how long that may be; past a threshold the panel takes the request, answers at once, and notifies the account when the file is ready.

func (ExportJob) TableName

func (ExportJob) TableName() string

type Field

type Field[T any] struct {
	// contains filtered or unexported fields
}

Field configures one form input; methods chain.

func (*Field[T]) Accept

func (fd *Field[T]) Accept(mimes string) *Field[T]

Accept sets the upload MIME filter ("image/*").

func (*Field[T]) CreationRules

func (fd *Field[T]) CreationRules(rules string) *Field[T]

CreationRules adds rules applied only when creating.

func (*Field[T]) Datetime

func (fd *Field[T]) Datetime() *Field[T]

Datetime carries a time through both ends of a DateRange, for a pair whose times mean something — an event running from 16:30 to 17:30. Both columns store a date and a time then, and the control asks for both.

f.DateRange("DateStart", "DateEnd", "Berlangsung").Datetime()

It has no effect on any other kind: Date and Datetime already say which they are.

func (*Field[T]) Default

func (fd *Field[T]) Default(v any) *Field[T]

Default supplies the initial value on the create form.

func (*Field[T]) Dir

func (fd *Field[T]) Dir(dir string) *Field[T]

Dir sets the upload subdirectory for File/Image fields.

func (*Field[T]) Disable

func (fd *Field[T]) Disable() *Field[T]

Disable renders the input disabled (and ignores submissions).

func (*Field[T]) Disk

func (fd *Field[T]) Disk(name string) *Field[T]

Disk names where a File or Image field stores what it is given, and where its value is read back from. Unset, the field uses Config.DefaultDisk.

func (*Field[T]) Help

func (fd *Field[T]) Help(s string) *Field[T]

Help renders hint text under the input.

func (*Field[T]) Max

func (fd *Field[T]) Max(v any) *Field[T]

Max sets the highest value a field accepts; see Min.

func (*Field[T]) MaxFiles

func (fd *Field[T]) MaxFiles(n int) *Field[T]

MaxFiles bounds how many a Files or Images field accepts (default 10).

func (*Field[T]) MaxSize

func (fd *Field[T]) MaxSize(n int64) *Field[T]

MaxSize caps upload size in bytes.

func (*Field[T]) Min

func (fd *Field[T]) Min(v any) *Field[T]

Min sets the lowest value a field accepts: a time.Time or a layout-shaped string for the temporal kinds, a number for the numeric ones.

It reaches the control as the browser's own min attribute and is checked again on save, because an attribute is a hint to whoever is using the page and no obstacle to anyone who is not.

func (*Field[T]) OnlyOnCreate

func (fd *Field[T]) OnlyOnCreate() *Field[T]

OnlyOnCreate shows the field only on the create form.

func (*Field[T]) OnlyOnUpdate

func (fd *Field[T]) OnlyOnUpdate() *Field[T]

OnlyOnUpdate shows the field only on the edit form.

func (*Field[T]) Options

func (fd *Field[T]) Options(o Options) *Field[T]

Options supplies choices for Select/Radio.

func (*Field[T]) OptionsFunc

func (fd *Field[T]) OptionsFunc(fn func(c *Context) Options) *Field[T]

OptionsFunc supplies choices lazily per request.

func (*Field[T]) Placeholder

func (fd *Field[T]) Placeholder(s string) *Field[T]

Placeholder sets the input placeholder.

func (*Field[T]) ReadOnly

func (fd *Field[T]) ReadOnly() *Field[T]

ReadOnly renders the input read-only.

func (*Field[T]) Required

func (fd *Field[T]) Required() *Field[T]

Required marks the field required (adds the rule and the asterisk).

func (*Field[T]) Rules

func (fd *Field[T]) Rules(rules string) *Field[T]

Rules sets Laravel-style validation ("required|max:255|unique:posts,title,{id}").

func (*Field[T]) SavingValue

func (fd *Field[T]) SavingValue(fn func(c *Context, raw string) (any, error)) *Field[T]

SavingValue transforms the raw submitted string before decoding — the per-field escape hatch (hashing passwords, normalizing input).

func (*Field[T]) Show

func (fd *Field[T]) Show(fn func(c *Context) bool) *Field[T]

Show gates the field on a per-request predicate — the seam for a form whose shape depends on who is filling it in:

f.Select("Status").Show(func(c *steward.Context) bool {
    return c.User.HasRole("editor")
})

A hidden field is skipped when the form renders *and* when a submission is decoded, so a caller who forges the input cannot write it. It is also omitted from the resource's _schema response, so a headless client is not told about a field it may not set.

Because the field never decodes, nothing writes the column — Default is a render-time value for the input and does not apply. Supply the value in a Saving hook (or leave it to the column's database default):

f.Select("Status").Options(...).Show(isEditor)
f.Saving(func(c *steward.Context, p *Post) error {
    if !isEditor(c) {
        p.Status = "draft"
    }
    return nil
})

func (*Field[T]) Span

func (fd *Field[T]) Span(n int) *Field[T]

Span sets how much of the form's width the field takes, in twelfths. A field spans the whole row unless told otherwise, so two Span(6) fields sit side by side and three Span(4) fields make a row of three. Values outside 1..12 are ignored.

The span applies from the "sm" breakpoint up. Below it every field is full width, because two controls side by side on a phone are two controls too narrow to use.

func (*Field[T]) Symbol

func (fd *Field[T]) Symbol(s string) *Field[T]

Symbol sets what a Currency field is prefixed with, overriding Config.CurrencySymbol for this field alone.

func (*Field[T]) UpdateRules

func (fd *Field[T]) UpdateRules(rules string) *Field[T]

UpdateRules adds rules applied only when updating.

func (*Field[T]) ValuesFunc

func (fd *Field[T]) ValuesFunc(fn func(c *Context, m any) []string) *Field[T]

ValuesFunc supplies the selected values for a MultiSelect on the edit form; m is the typed row (assert to *T).

type FieldKind

type FieldKind int

FieldKind selects a form field's input widget and decode behavior.

const (
	FieldText FieldKind = iota
	FieldTextarea
	FieldEmail
	FieldPassword
	FieldURL
	FieldNumber
	FieldDecimal
	FieldCurrency
	FieldHidden
	FieldDisplay
	FieldSelect
	FieldRadio
	FieldSwitch
	FieldColor
	FieldDate
	FieldDatetime
	FieldTime
	FieldFile
	FieldImage
	FieldMarkdown
	FieldBelongsTo
	FieldMultiSelect
	FieldRichtext
	FieldIcon
	FieldFiles
	FieldImages
)

Form field kinds available in v1.

type FilterItem

type FilterItem[T any] struct {
	// contains filtered or unexported fields
}

FilterItem is one filter control.

func (*FilterItem[T]) Datetime

func (fi *FilterItem[T]) Datetime() *FilterItem[T]

Datetime switches a Between filter to date-range inputs. Datetime asks for a time alongside the date.

On a DateRange filter both ends carry one, and the bounds are then exact rather than rounded out to whole days. On any other filter it becomes a single date-and-time picker.

It is refused on Between, which is the numeric two-ended filter: a range of dates is DateRange, with or without times.

func (*FilterItem[T]) Placeholder

func (fi *FilterItem[T]) Placeholder(s string) *FilterItem[T]

Placeholder sets the input placeholder.

func (*FilterItem[T]) Select

func (fi *FilterItem[T]) Select(o Options) *FilterItem[T]

Select renders the filter as a dropdown of options.

func (*FilterItem[T]) SelectFunc

func (fi *FilterItem[T]) SelectFunc(fn func(*Context) Options) *FilterItem[T]

SelectFunc renders the filter as a dropdown whose options are resolved per request. Select takes its map once, when the resource is registered, so a list read from the database there is both loaded at boot and never refreshed.

func (*FilterItem[T]) Span

func (fi *FilterItem[T]) Span(n int) *FilterItem[T]

Span sets how many of the filter panel's twelve columns the control takes, the same twelve a form divides into. Values outside 1..12 are clamped.

f.Equal("Status").Span(3)
f.Between("PostDate").Datetime().Span(6)

Unset, each kind takes a width that suits it: a range needs room for two controls, a switch needs almost none.

type Filters

type Filters[T any] struct {
	// contains filtered or unexported fields
}

Filters declares the filter panel inside Grid.Filter.

func (*Filters[T]) Between

func (f *Filters[T]) Between(path string, label ...string) *FilterItem[T]

Between filters lo ≤ path ≤ hi with two inputs.

func (*Filters[T]) Date

func (f *Filters[T]) Date(path string, label ...string) *FilterItem[T]

Date filters a date column by day.

func (*Filters[T]) DateRange

func (f *Filters[T]) DateRange(path string, label ...string) *FilterItem[T]

DateRange filters a date column by a range chosen in one calendar, rather than two separate inputs. It submits the same two parameters a Between filter does — {param} and {param}_to — so a half-open range still works and a URL written by hand behaves the same.

func (*Filters[T]) Equal

func (f *Filters[T]) Equal(path string, label ...string) *FilterItem[T]

Equal filters path = value.

func (*Filters[T]) Gt

func (f *Filters[T]) Gt(path string, label ...string) *FilterItem[T]

Gt filters path > value.

func (*Filters[T]) In

func (f *Filters[T]) In(path string, label ...string) *FilterItem[T]

In filters path IN (values) — pair with Select+multiple later milestones.

func (*Filters[T]) Like

func (f *Filters[T]) Like(path string, label ...string) *FilterItem[T]

Like filters path LIKE %value%.

func (*Filters[T]) Lt

func (f *Filters[T]) Lt(path string, label ...string) *FilterItem[T]

Lt filters path < value.

type Form

type Form[T any] struct {
	// contains filtered or unexported fields
}

Form configures a resource's create/edit view; write-only until Build. Hooks receive the typed model, never a map.

func (*Form[T]) BelongsTo

func (f *Form[T]) BelongsTo(fkPath, relation, titleField string, label ...string) *Field[T]

BelongsTo binds a foreign-key field to a searchable select over the relation: BelongsTo("AuthorID", "Author", "Name").

func (*Form[T]) Color

func (f *Form[T]) Color(path string, label ...string) *Field[T]

Color adds a color picker storing "#rrggbb".

func (*Form[T]) Currency

func (f *Form[T]) Currency(path string, label ...string) *Field[T]

Currency adds a decimal input with a currency prefix.

func (*Form[T]) Date

func (f *Form[T]) Date(path string, label ...string) *Field[T]

Date adds a date picker (stored midnight local).

func (*Form[T]) DateRange

func (f *Form[T]) DateRange(fromPath, toPath string, label ...string) *Field[T]

DateRange pairs two date columns into one control: one calendar, both ends, with the same behaviour a grid filter's range has — on a touch device the two native inputs appear instead, since a calendar built here beats nothing the platform offers.

f.DateRange("DateStart", "DateEnd", "Berlangsung")

Each end keeps its own column, so validation, defaults and the change log treat them as the two fields they are. Rules set on the returned field apply to the start; UpdateRules and the rest chain as usual.

func (*Form[T]) Datetime

func (f *Form[T]) Datetime(path string, label ...string) *Field[T]

Datetime adds a date+time picker.

func (*Form[T]) Decimal

func (f *Form[T]) Decimal(path string, label ...string) *Field[T]

Decimal adds a decimal-number input.

func (*Form[T]) Deleted

func (f *Form[T]) Deleted(fn func(c *Context, ids []string) error) *Form[T]

Deleted runs after rows are deleted.

func (*Form[T]) Deleting

func (f *Form[T]) Deleting(fn func(c *Context, ids []string) error) *Form[T]

Deleting runs before rows are deleted.

func (*Form[T]) Display

func (f *Form[T]) Display(path string, label ...string) *Field[T]

Display shows the value read-only and never persists it.

func (*Form[T]) Divider

func (f *Form[T]) Divider()

Divider inserts a horizontal rule between fields.

func (*Form[T]) Email

func (f *Form[T]) Email(path string, label ...string) *Field[T]

Email adds an email input (browser + server validation).

func (*Form[T]) Fieldset

func (f *Form[T]) Fieldset(title string, fn func(*Form[T]))

Fieldset groups the fields declared inside fn under a legend.

func (*Form[T]) File

func (f *Form[T]) File(path string, label ...string) *Field[T]

File adds an upload field storing the file path.

func (*Form[T]) Files

func (f *Form[T]) Files(path string, label ...string) *Field[T]

Files adds a multi-file upload. The column holds a JSON array of storage paths, in the order they were added.

It is for files the panel shows and hands over and never asks questions about: a JSON column cannot be filtered, sorted or counted in SQL. Where the panel should list them, caption them or order them, model each as a row and use HasMany instead.

func (*Form[T]) Hidden

func (f *Form[T]) Hidden(path string, label ...string) *Field[T]

Hidden adds a hidden input.

func (*Form[T]) Icon

func (f *Form[T]) Icon(path string, label ...string) *Field[T]

Icon adds a visual picker over the icons the panel can render, storing the chosen name as a string.

It exists because the alternative — a text input holding an icon name — fails silently: a typo renders a blank space, and nobody notices until a sidebar looks wrong. The picker can only produce a name that resolves.

Choices come from the asset layers, so icons dropped into Config.AssetsFS appear alongside the embedded set.

func (*Form[T]) Image

func (f *Form[T]) Image(path string, label ...string) *Field[T]

Image adds an image upload with preview.

func (*Form[T]) Images

func (f *Form[T]) Images(path string, label ...string) *Field[T]

Images is Files for pictures: the same JSON array, thumbnails instead of names, and the image-only rule File carries.

func (*Form[T]) Markdown

func (f *Form[T]) Markdown(path string, label ...string) *Field[T]

Markdown adds a markdown editor (textarea with preview styling).

func (*Form[T]) MultiSelect

func (f *Form[T]) MultiSelect(name string, label ...string) *Field[T]

MultiSelect adds a multiple-choice select. It is virtual by default: the submitted values never write to a model column — read them in a Saved hook via c.R.Form[name] (used for pivot syncing, tags, etc.). Supply the current selection with ValuesFunc.

func (*Form[T]) Number

func (f *Form[T]) Number(path string, label ...string) *Field[T]

Number adds an integer input.

func (*Form[T]) Password

func (f *Form[T]) Password(path string, label ...string) *Field[T]

Password adds a password input; empty submissions are ignored on edit.

func (*Form[T]) Radio

func (f *Form[T]) Radio(path string, label ...string) *Field[T]

Radio adds a radio group.

func (*Form[T]) Richtext

func (f *Form[T]) Richtext(path string, label ...string) *Field[T]

Richtext adds a small WYSIWYG editor storing HTML: bold, italic, underline, headings, lists, links, blockquote, and clear-formatting.

Submitted markup is sanitized server-side against an allowlist of tags and attributes (see internal/htmlsafe), because a contenteditable field is an arbitrary-HTML input and the value is rendered back with Detail.HTML. Anything outside the allowlist is dropped, so a compromised or scripted client cannot store markup that later executes in another admin's browser.

It is deliberately modest. A field that needs image handling, tables, or pasted-Word cleanup wants a dedicated editor, which belongs in the app rather than vendored into the framework.

func (*Form[T]) Saved

func (f *Form[T]) Saved(fn func(c *Context, m *T, created bool) error) *Form[T]

Saved runs after successful persistence.

func (*Form[T]) Saving

func (f *Form[T]) Saving(fn func(c *Context, m *T) error) *Form[T]

Saving runs after validation on the populated model, before persistence.

func (*Form[T]) Select

func (f *Form[T]) Select(path string, label ...string) *Field[T]

Select adds a dropdown; supply Options or OptionsFunc.

func (*Form[T]) Submitted

func (f *Form[T]) Submitted(fn func(c *Context) error) *Form[T]

Submitted runs before input decoding; returning an error aborts with it.

func (*Form[T]) Switch

func (f *Form[T]) Switch(path string, label ...string) *Field[T]

Switch adds an on/off toggle bound to a bool field.

func (*Form[T]) Text

func (f *Form[T]) Text(path string, label ...string) *Field[T]

Text adds a single-line text input.

func (*Form[T]) Textarea

func (f *Form[T]) Textarea(path string, label ...string) *Field[T]

Textarea adds a multi-line input.

func (*Form[T]) Time

func (f *Form[T]) Time(path string, label ...string) *Field[T]

func (*Form[T]) URL

func (f *Form[T]) URL(path string, label ...string) *Field[T]

URL adds a URL input.

func (*Form[T]) Width

func (f *Form[T]) Width(w FormWidth) *Form[T]

Width caps how wide the form grows. The default suits a single column of fields; a form using Span to put several on a row usually wants more.

type FormWidth

type FormWidth string

FormWidth caps how wide a form grows on a large screen.

const (
	// FormNarrow suits a form of a few short fields, where full width would
	// leave the labels stranded a long way from their controls.
	FormNarrow FormWidth = "narrow"
	// FormNormal is the default.
	FormNormal FormWidth = "normal"
	// FormWide suits a form with several fields to a row.
	FormWide FormWidth = "wide"
	// FormFull uses whatever the page gives it.
	FormFull FormWidth = "full"
)

type GormRepository

type GormRepository[T any] struct {
	// contains filtered or unexported fields
}

GormRepository is the default Repository backed by a *gorm.DB. Field paths in queries resolve through the model's parsed schema; one-hop relation paths referenced by grid columns are preloaded automatically by the grid, not here.

func NewGormRepository

func NewGormRepository[T any](db *gorm.DB) (*GormRepository[T], error)

NewGormRepository builds the default repository for T.

func (*GormRepository[T]) Aggregate

func (r *GormRepository[T]) Aggregate(ctx context.Context, q *AggQuery) (AggRows, error)

Aggregate implements Aggregator over GORM.

func (*GormRepository[T]) Create

func (r *GormRepository[T]) Create(ctx context.Context, m *T) error

Create implements Repository.

func (*GormRepository[T]) Delete

func (r *GormRepository[T]) Delete(ctx context.Context, ids []string) error

Delete implements Repository (hard delete; ids match the primary key).

func (*GormRepository[T]) Find

func (r *GormRepository[T]) Find(ctx context.Context, id string) (*T, error)

Find implements Repository.

func (*GormRepository[T]) KeyName

func (r *GormRepository[T]) KeyName() string

KeyName implements Repository.

func (*GormRepository[T]) List

func (r *GormRepository[T]) List(ctx context.Context, q *ListQuery) ([]T, int64, error)

List implements Repository.

func (*GormRepository[T]) Query

func (r *GormRepository[T]) Query(fn func(*gorm.DB) *gorm.DB) *GormRepository[T]

Query appends a refinement applied to every statement — the escape hatch for base scoping ("only rows of this tenant").

func (*GormRepository[T]) Update

func (r *GormRepository[T]) Update(ctx context.Context, m *T, fields []string) error

Update implements Repository; fields are model field paths (converted to columns), nil updates all non-zero the GORM way — builders always pass the dirty list explicitly.

func (*GormRepository[T]) With

func (r *GormRepository[T]) With(relations ...string) *GormRepository[T]

With adds relations to preload on List and Find.

type Grid

type Grid[T any] struct {
	// contains filtered or unexported fields
}

Grid configures a resource's list view. Obtained inside Resource[T].Grid(func(g *Grid[T]) { ... }); write-only until Build.

func (*Grid[T]) ActionStyle

func (g *Grid[T]) ActionStyle(style GridActionStyle) *Grid[T]

ActionStyle overrides Config.GridActions for this grid.

Use it where one resource does not fit the panel-wide choice — a grid with a single action rarely needs a menu, and a grid with six rarely wants them spread across the row.

func (*Grid[T]) BatchAction

func (g *Grid[T]) BatchAction(a *Action) *Grid[T]

BatchAction adds an action over the selected rows.

func (*Grid[T]) Column

func (g *Grid[T]) Column(path string, label ...string) *Column[T]

Column adds a model field column ("Title", "Author.Name"); the optional second argument overrides the derived label.

func (*Grid[T]) ColumnFunc

func (g *Grid[T]) ColumnFunc(name, label string, fn func(row *T) template.HTML) *Column[T]

ColumnFunc adds a computed column rendered entirely by fn.

func (*Grid[T]) DefaultSort

func (g *Grid[T]) DefaultSort(path string, desc bool) *Grid[T]

DefaultSort orders the grid before the user picks a column.

func (*Grid[T]) DisableCreate

func (g *Grid[T]) DisableCreate() *Grid[T]

DisableCreate hides the create button.

func (*Grid[T]) DisableDelete

func (g *Grid[T]) DisableDelete() *Grid[T]

DisableDelete hides row delete actions and batch delete.

func (*Grid[T]) DisableEdit

func (g *Grid[T]) DisableEdit() *Grid[T]

DisableEdit hides row edit actions.

func (*Grid[T]) DisableExport

func (g *Grid[T]) DisableExport() *Grid[T]

DisableExport hides CSV export.

func (*Grid[T]) DisableFilter

func (g *Grid[T]) DisableFilter() *Grid[T]

DisableFilter hides the filter panel.

func (*Grid[T]) DisablePagination

func (g *Grid[T]) DisablePagination() *Grid[T]

DisablePagination shows all rows.

func (*Grid[T]) DisableQuickSearch

func (g *Grid[T]) DisableQuickSearch() *Grid[T]

DisableQuickSearch hides the search box even when paths are set.

func (*Grid[T]) DisableRowSelector

func (g *Grid[T]) DisableRowSelector() *Grid[T]

DisableRowSelector hides checkboxes (and with them batch actions).

func (*Grid[T]) DisableView

func (g *Grid[T]) DisableView() *Grid[T]

DisableView hides the row detail action.

func (*Grid[T]) Filter

func (g *Grid[T]) Filter(fn func(*Filters[T])) *Grid[T]

Filter declares the filter panel.

func (*Grid[T]) FilterLayout

func (g *Grid[T]) FilterLayout(l GridFilterLayout) *Grid[T]

FilterLayout overrides Config.FilterLayout for this grid.

func (*Grid[T]) GroupColumns

func (g *Grid[T]) GroupColumns(label string, paths ...string) *Grid[T]

GroupColumns spans a header label over the named columns, which must be contiguous in declaration order (verified at Build). The column picker is disabled on grids with grouped headers.

func (*Grid[T]) PerPage

func (g *Grid[T]) PerPage(def int, options ...int) *Grid[T]

PerPage sets the default page size and the selector options.

func (*Grid[T]) QuickSearch

func (g *Grid[T]) QuickSearch(paths ...string) *Grid[T]

QuickSearch enables the search box over the given field paths, with the dcat mini-DSL (field:value, %contains%, >n, (a,b), [lo,hi], NULL).

func (*Grid[T]) Reorderable

func (g *Grid[T]) Reorderable(url string) *Grid[T]

Reorderable makes rows draggable; on drop the new order posts the row keys (form field "ids", comma-separated, top to bottom) to url. Pair it with a Resource.Page handler that persists the order.

func (*Grid[T]) RowAction

func (g *Grid[T]) RowAction(a *Action) *Grid[T]

RowAction adds a per-row action button.

func (*Grid[T]) ToolAction

func (g *Grid[T]) ToolAction(a *Action) *Grid[T]

ToolAction adds a toolbar action (no row context).

func (*Grid[T]) Tree

func (g *Grid[T]) Tree(parentPath string) *Grid[T]

Tree renders rows as a collapsible hierarchy over the given parent-key field ("ParentID"). The whole tree loads at once (up to 1000 rows) in depth-first order; quick search and filters fall back to the flat list.

type GridActionStyle

type GridActionStyle string

GridActionStyle selects how a row's actions are presented.

const (
	// GridActionsButtons lays the actions out side by side. Fastest to reach,
	// and the default, but it costs a column's width per action.
	GridActionsButtons GridActionStyle = "buttons"

	// GridActionsMenu collapses them behind a single trigger. Worth it once a
	// grid has several actions or many columns, at the price of one extra click.
	GridActionsMenu GridActionStyle = "menu"
)

type GridFilterLayout

type GridFilterLayout string

GridFilterLayout chooses where a grid's filter panel lives.

const (
	// FiltersAbove puts the panel between the toolbar and the rows, open in
	// place. The default.
	FiltersAbove GridFilterLayout = "above"
	// FiltersDrawer puts it in a drawer from the right, over the page. Worth it
	// where a grid has enough filters that opening them in place pushes the
	// rows off the screen.
	FiltersDrawer GridFilterLayout = "drawer"
)

type IntervalScheduler

type IntervalScheduler struct {
	// contains filtered or unexported fields
}

IntervalScheduler runs jobs on intervals or cron expressions. Specs: "@every 10m" (any time.ParseDuration string), "@hourly", "@daily", "@weekly", or a standard five-field cron expression ("30 2 * * 1-5", minute resolution).

func NewIntervalScheduler

func NewIntervalScheduler() *IntervalScheduler

NewIntervalScheduler returns a scheduler; Start it once jobs are added.

func (*IntervalScheduler) Add

func (s *IntervalScheduler) Add(spec, name string, fn func(context.Context) error) error

Add implements Scheduler.

func (*IntervalScheduler) Jobs

func (s *IntervalScheduler) Jobs() []JobInfo

Jobs implements Scheduler.

func (*IntervalScheduler) Start

func (s *IntervalScheduler) Start(ctx context.Context)

Start launches all jobs until ctx is done.

type JobInfo

type JobInfo struct {
	Name    string
	Spec    string
	LastRun time.Time
	LastErr string
}

JobInfo describes a scheduled job for the admin page.

type ListQuery

type ListQuery struct {
	Conds       []Cond
	Search      string
	SearchConds []Cond // parsed quick-search terms (field-targeted)
	SearchPaths []string
	Sorts       []Sort
	// SkipCount drops the COUNT that pages a grid. A caller that shows a fixed
	// few rows and never paginates pays for it twice over: on a large table the
	// count scans every match while the rows themselves stop at the limit.
	// Total is 0 when it is set.
	SkipCount bool

	// IDOrder ranks the rows by primary key, most relevant first, and is what a
	// search engine's answer arrives as. It orders before Sorts, because the
	// order a person asked for beats the order they did not — a chosen column
	// sort is added to Sorts and takes precedence by not setting this at all.
	//
	// Without it the ranking is thrown away: the IDs go in as an IN condition
	// and the database returns them in whatever order it likes, so "the 1000
	// best matches" becomes "10 of them, by id".
	IDOrder []string
	Page    int
	PerPage int

	// After pages by primary key rather than by OFFSET: rows come back ordered
	// by key ascending, starting past this value, and Sorts and Page are
	// ignored. It exists for walking a whole table — OFFSET re-reads every row
	// it has already skipped, so the last page of a large export costs as much
	// as all of the ones before it.
	//
	// Nil means offset paging. A caller walks with the last key it saw.
	After  any
	Scopes []any
}

ListQuery describes one grid page load: filters, quick search, ordering, and pagination. Scopes carry backend-specific query refinements (row policies, filter scopes) — for the GORM repository they are func(*gorm.DB) *gorm.DB, passed through untyped so the interface stays backend-neutral.

type LocalStorage

type LocalStorage struct {
	Dir     string
	BaseURL string

	// SigningKey signs time-limited URLs. The Admin sets it from Config.SecretKey
	// when it wires up the backend; left empty, SignedURL reports ErrNotSigned
	// and links fall back to the authenticated route.
	SigningKey []byte
	// contains filtered or unexported fields
}

LocalStorage stores uploads on the local filesystem below Dir and serves them under BaseURL (the admin wires BaseURL to {prefix}/_uploads).

func (*LocalStorage) Delete

func (s *LocalStorage) Delete(_ context.Context, name string) error

Delete implements Storage; deleting a missing file is not an error.

func (*LocalStorage) Put

func (s *LocalStorage) Put(_ context.Context, name string, r io.Reader, _ int64, _ string) (string, error)

Put implements Storage.

func (*LocalStorage) SignedURL

func (s *LocalStorage) SignedURL(_ context.Context, name string, ttl time.Duration) (string, error)

SignedURL implements SignedURLStorage. The signature is made with the panel's own secret, so a local upload can be linked to for a while without the route that serves it being open to everyone.

func (*LocalStorage) URL

func (s *LocalStorage) URL(name string) string

URL implements Storage.

type Mail

type Mail struct {
	To      []string
	Subject string
	HTML    string
	Text    string
}

Mail is one outbound message.

type Mailer

type Mailer interface {
	Send(ctx context.Context, m Mail) error
}

Mailer sends mail; configuring one enables the password-reset flow.

type MemoryCache

type MemoryCache struct {
	// contains filtered or unexported fields
}

MemoryCache is a TTL map cache; the zero value is not usable, call NewMemoryCache.

func NewMemoryCache

func NewMemoryCache() *MemoryCache

NewMemoryCache returns an empty in-process cache.

func (*MemoryCache) Delete

func (c *MemoryCache) Delete(_ context.Context, keys ...string) error

Delete implements Cache.

func (*MemoryCache) Get

func (c *MemoryCache) Get(_ context.Context, key string) ([]byte, bool, error)

Get implements Cache.

func (*MemoryCache) Set

func (c *MemoryCache) Set(_ context.Context, key string, val []byte, ttl time.Duration) error

Set implements Cache; ttl <= 0 stores without expiry.

type MemorySearcher

type MemorySearcher struct {
	// contains filtered or unexported fields
}

MemorySearcher is a simple in-process inverted index: lowercase word tokens, AND across the query's terms, ranked by summed term frequency.

It is here so the Searcher path can be exercised without standing an engine up — in a test, or on a panel small enough not to need one. It holds everything in memory and rebuilds from nothing on restart, so a panel that depends on search wants a real engine behind the same interface.

func NewMemorySearcher

func NewMemorySearcher() *MemorySearcher

NewMemorySearcher returns an empty index.

func (*MemorySearcher) Delete

func (m *MemorySearcher) Delete(_ context.Context, typ string, ids ...string) error

Delete implements Searcher.

func (*MemorySearcher) Index

func (m *MemorySearcher) Index(_ context.Context, docs ...SearchDoc) error

Index implements Searcher.

func (*MemorySearcher) Query

func (m *MemorySearcher) Query(_ context.Context, typ, q string, limit int) ([]SearchHit, error)

Query implements Searcher: every term must match, and only documents of the type asked for are returned.

type MenuItem struct {
	ID         uint       `gorm:"primaryKey"`
	ParentID   uint       `gorm:"default:0"`
	Order      int        `gorm:"default:0"`
	Title      string     `gorm:"size:100"`
	Icon       string     `gorm:"size:100"`
	URI        string     `gorm:"size:255"`
	Show       bool       `gorm:"default:true"`
	Source     MenuSource `gorm:"size:10;default:db"`
	CodeKey    *string    `gorm:"size:100;uniqueIndex"`
	Overridden bool       `gorm:"default:false"`
	CreatedAt  time.Time
	UpdatedAt  time.Time
}

MenuItem is one sidebar node. CodeKey ties a "code" row to its resource slug; Overridden marks rows edited in the UI so sync leaves them alone.

func (MenuItem) TableName() string
type MenuNode struct {
	Title    string
	Icon     string
	URI      string // absolute path ("" for group headers)
	Active   bool
	Children []MenuNode
}

MenuNode is one rendered sidebar entry.

type MenuSection struct {
	Title  string // "" for a run of ungrouped links
	Items  []MenuNode
	Active bool
}

MenuSection is one sidebar block: either a labelled group with its entries, or a run of consecutive top-level links carrying no label.

type MenuSource string

MenuSource records who owns a menu row: rows synced from registered resources ("code") reconcile on boot; hand-created rows ("db") are never touched by sync.

const (
	MenuSourceCode MenuSource = "code"
	MenuSourceDB   MenuSource = "db"
)

type MetricNode

type MetricNode struct {
	// contains filtered or unexported fields
}

MetricNode is one figure and its label. Icon and Color chain off Metric.

func Metric

func Metric(label string, value any, hint ...string) *MetricNode

Metric shows one figure with its label, the same tile the dashboard uses. hint is optional secondary text beneath the value.

steward.Metric("Published", 1752, "live on the site").
    Icon("newspaper").Color(steward.BadgeGreen)

func (*MetricNode) Color

func (m *MetricNode) Color(c BadgeColor) *MetricNode

Color tints the card and the icon. It takes the panel's colour vocabulary, the same one badges use, and an unknown colour is reported by Verify.

func (*MetricNode) Icon

func (m *MetricNode) Icon(name string) *MetricNode

Icon draws a Lucide glyph beside the figure, in the tile's colour. The name is checked at boot, so a glyph that does not exist is a build error rather than an empty square.

type Node

type Node interface {
	// contains filtered or unexported methods
}

Node is one element of a page layout: a row, a column, or something to show. The interface is closed — the constructors in this file are the only implementations — so a layout is always a shape the renderer understands.

func Card

func Card(title string, children ...Node) Node

Card wraps its children in the panel's card, with an optional heading. An empty title renders the card without a header.

func Chart

func Chart(data *ChartData) Node

Chart draws a series with the same component the dashboard's chart tiles use, and needs the same runtime in the page — Context.Layout includes it whenever the tree holds one.

func Col

func Col(span int, children ...Node) Node

Col occupies span of the row's twelve columns and stacks its children. A span outside 1..12 is clamped.

func Divider

func Divider() Node

Divider draws a rule between sections.

func Heading

func Heading(s string) Node

Heading renders a section heading above whatever follows it.

func Markup

func Markup(h template.HTML) Node

Markup places markup you have already built. It is not sanitized: pass template.HTML you produced, not a value that came from a request.

func Row

func Row(cols ...Node) Node

Row places its columns side by side. Columns whose spans exceed twelve wrap onto the next line, and below the small breakpoint each takes the full width.

steward.Row(
    steward.Col(8, steward.Card("Trend", chart)),
    steward.Col(4, steward.Card("Totals", totals)),
)

func Table

func Table(headers []string, rows [][]any) Node

Table renders rows under headers. Values are escaped, so a cell holding markup is shown as text; build the markup with Markup for a cell that should render.

func Text

func Text(s string) Node

Text renders escaped body text.

type Notification

type Notification struct {
	ID     uint   `gorm:"primaryKey"`
	UserID uint   `gorm:"not null;index:idx_notif_user_read,priority:1"`
	Type   string `gorm:"size:120;index"`
	Title  string `gorm:"size:255;not null"`
	Body   string `gorm:"type:text"`
	URL    string `gorm:"size:512"`
	Icon   string `gorm:"size:60"`
	Data   string `gorm:"type:text"`

	// Null until read. Indexed with UserID because every query here is "this
	// user's, unread first".
	ReadAt    *time.Time `gorm:"index:idx_notif_user_read,priority:2"`
	CreatedAt time.Time
}

Notification is a message addressed to one panel account, stored in the database and read from the bell in the header.

Title, Body, URL and Icon are what the panel renders. Data is for the caller: an arbitrary JSON payload the panel never interprets, so a handler reading a notification back can recover what it was about without parsing the prose.

func (*Notification) Payload

func (n *Notification) Payload(v any) error

Payload unmarshals Data into v. It is a no-op when Data is empty, so a notification stored without one does not have to be special-cased.

func (*Notification) Read

func (n *Notification) Read() bool

Read reports whether the notification has been read.

func (Notification) TableName

func (Notification) TableName() string

func (Notification) WithPayload

func (n Notification) WithPayload(v any) Notification

WithPayload marshals v into Data and returns the notification, for use inline in a Notify call.

type Op

type Op string

Op is a filter comparison operator.

const (
	OpEq      Op = "eq"
	OpNe      Op = "ne"
	OpGt      Op = "gt"
	OpGte     Op = "gte"
	OpLt      Op = "lt"
	OpLte     Op = "lte"
	OpLike    Op = "like"
	OpPrefix  Op = "prefix"
	OpIn      Op = "in"
	OpBetween Op = "between"
	OpNull    Op = "null"
)

Filter operators supported by ListQuery conditions.

type OperationLog

type OperationLog struct {
	ID        uint      `gorm:"primaryKey"`
	UserID    uint      `gorm:"index"`
	Path      string    `gorm:"size:255"`
	Method    string    `gorm:"size:10"`
	IP        string    `gorm:"size:45"`
	Input     string    `gorm:"type:text"`
	CreatedAt time.Time `gorm:"index"`
}

OperationLog records one admin request (input is masked JSON).

func (OperationLog) TableName

func (OperationLog) TableName() string

type Options

type Options map[string]string

Options maps stored values to display labels for selects/radios.

type Period

type Period string

Period buckets a date or timestamp column.

Week is deliberately absent: the three supported engines disagree on where a week starts and on week numbering, and only SQLite is covered by integration tests here, so shipping it would mean three subtly different definitions.

const (
	PeriodDay   Period = "day"
	PeriodMonth Period = "month"
	PeriodYear  Period = "year"
)

type Permission

type Permission struct {
	ID         uint   `gorm:"primaryKey"`
	Name       string `gorm:"size:50"`
	Slug       string `gorm:"size:50;uniqueIndex"`
	HTTPMethod string `gorm:"size:255"`
	HTTPPath   string `gorm:"type:text"`
	Order      int    `gorm:"default:0"`
	ParentID   uint   `gorm:"default:0"`
	CreatedAt  time.Time
	UpdatedAt  time.Time
}

Permission is a named grant matched against HTTP requests. HTTPMethod is a comma-separated method list (empty = any); HTTPPath is a newline-separated list of path patterns with * globs, each optionally prefixed "GET,POST:".

func (Permission) TableName

func (Permission) TableName() string

type PermissionMenu

type PermissionMenu struct {
	PermissionID uint `gorm:"primaryKey"`
	MenuID       uint `gorm:"primaryKey"`
	CreatedAt    time.Time
	UpdatedAt    time.Time
}

PermissionMenu binds menu rows to permissions.

func (PermissionMenu) TableName

func (PermissionMenu) TableName() string

type Policy

type Policy[T any] interface {
	ViewAny(c *Context) bool
	View(c *Context, m *T) bool
	Create(c *Context) bool
	Update(c *Context, m *T) bool
	Delete(c *Context, m *T) bool
}

Policy gates one resource's actions for the current user. Menu visibility derives from ViewAny — one source of truth instead of dcat's separate role↔menu bookkeeping.

type Repository

type Repository[T any] interface {
	Find(ctx context.Context, id string) (*T, error)
	List(ctx context.Context, q *ListQuery) (items []T, total int64, err error)
	Create(ctx context.Context, m *T) error
	// Update persists the named fields (all when nil).
	Update(ctx context.Context, m *T, fields []string) error
	Delete(ctx context.Context, ids []string) error
	KeyName() string
}

Repository is the data seam Grid, Form, and Detail render through. The GORM implementation is the default; anything that can list, fetch, and mutate records — a REST API, a file store — can back a resource.

type Resource

type Resource[T any] struct {
	// contains filtered or unexported fields
}

Resource is the public, generic handle returned by Register. All configuration happens through it before Build; the internal registry only ever sees the type-erased resourceEntry.

func Register

func Register[T any](a *Admin) *Resource[T]

Register adds the model T to the panel. With no further configuration the resource gets a slug and title derived from the type name; Grid, Form, and Detail builders arrive with later milestones and hang off this handle.

func (*Resource[T]) Command

func (r *Resource[T]) Command(paths ...string) *Resource[T]

Command makes the resource searchable from the command palette, over the paths given.

posts.Command("Title", "Slug")

It is opt-in rather than following QuickSearch, because the palette queries on every keystroke and a LIKE over a table nobody meant to include is paid for by every search that misses. Measured on one panel: eleven resources searched automatically cost 1.5–4.2s per keystroke, the worst of it on queries that matched nothing.

func (*Resource[T]) CommandDisplay

func (r *Resource[T]) CommandDisplay(paths ...string) *Resource[T]

CommandDisplay names what a palette row reads, rather than letting it be guessed from the grid's first two text columns. The first path is the line the reader reads; the rest join into the dimmer line beside it.

posts.Command("Title").CommandDisplay("Title", "Category.Name", "PostDate")

A path may cross one relation, and is loaded for you.

func (*Resource[T]) Detail

func (r *Resource[T]) Detail(fn func(*Detail[T])) *Resource[T]

Detail declares the show view; without it every direct field renders with its type default.

func (*Resource[T]) Form

func (r *Resource[T]) Form(fn func(*Form[T])) *Resource[T]

Form declares the create/edit view; without it every writable direct field gets an input inferred from its type.

func (*Resource[T]) Grid

func (r *Resource[T]) Grid(fn func(*Grid[T])) *Resource[T]

Grid declares the list view; fn runs at Build time against a fresh builder. Without it the grid shows every direct model field.

func (*Resource[T]) Group

func (r *Resource[T]) Group(name string) *Resource[T]

Group places the resource under a collapsible sidebar group.

func (*Resource[T]) Icon

func (r *Resource[T]) Icon(name string) *Resource[T]

Icon sets the sidebar icon by name, resolved through the asset layers ("news", "users", …). Verify reports a name that does not resolve, because at runtime an unknown icon renders blank rather than failing — which is easy to miss until a sidebar looks wrong.

func (*Resource[T]) Page

func (r *Resource[T]) Page(method, rel string, h func(c *Context) error) *Resource[T]

Page mounts a custom route under the resource ("_order" → {prefix}/{slug}/_order). The handler runs inside the standard middleware chain (auth, CSRF, permissions).

func (*Resource[T]) Policy

func (r *Resource[T]) Policy(p Policy[T]) *Resource[T]

Policy attaches fine-grained authorization. ViewAny gates the grid, its JSON listing, exports, and the sidebar entry; Create gates the create form and submissions; View/Update/Delete receive the loaded row, so ownership checks live here. Implement RowScoper on the same value to narrow every list query. Policies bind every user, administrators included — they express business rules, not role membership (use permissions for that).

func (*Resource[T]) Repository

func (r *Resource[T]) Repository(repo Repository[T]) *Resource[T]

Repository swaps the data source (default: GORM repository over Config.DB).

func (*Resource[T]) Searchable

func (r *Resource[T]) Searchable(paths ...string) *Resource[T]

Searchable declares which paths go into the search index, and turns quick search and the command palette over to Config.Searcher for this resource.

posts.Searchable("Title", "Body")

Without a Searcher configured this does nothing — the SQL LIKE path stays. The two are deliberately separate: an app can declare what its records are findable by and decide later what answers the query.

func (*Resource[T]) Slug

func (r *Resource[T]) Slug(s string) *Resource[T]

Slug overrides the URL segment (default: snake-cased plural of the type).

func (*Resource[T]) Title

func (r *Resource[T]) Title(s string) *Resource[T]

Title overrides the human name shown in menu and headings.

type Role

type Role struct {
	ID        uint   `gorm:"primaryKey"`
	Name      string `gorm:"size:50"`
	Slug      string `gorm:"size:50;uniqueIndex"`
	CreatedAt time.Time
	UpdatedAt time.Time

	Permissions []Permission `gorm:"many2many:admin_role_permissions;joinForeignKey:role_id;joinReferences:permission_id"`
}

Role groups permissions and users.

func (Role) TableName

func (Role) TableName() string

type RoleMenu

type RoleMenu struct {
	RoleID    uint `gorm:"primaryKey"`
	MenuID    uint `gorm:"primaryKey"`
	CreatedAt time.Time
	UpdatedAt time.Time
}

RoleMenu hides menu rows from specific roles (explicit override; visibility normally derives from policies).

func (RoleMenu) TableName

func (RoleMenu) TableName() string

type RolePermission

type RolePermission struct {
	RoleID       uint `gorm:"primaryKey"`
	PermissionID uint `gorm:"primaryKey"`
	CreatedAt    time.Time
	UpdatedAt    time.Time
}

RolePermission links roles to permissions.

func (RolePermission) TableName

func (RolePermission) TableName() string

type RoleUser

type RoleUser struct {
	RoleID    uint `gorm:"primaryKey"`
	UserID    uint `gorm:"primaryKey"`
	CreatedAt time.Time
	UpdatedAt time.Time
}

RoleUser links users to roles.

func (RoleUser) TableName

func (RoleUser) TableName() string

type RowScoper

type RowScoper interface {
	Scope(c *Context, db *gorm.DB) *gorm.DB
}

RowScoper optionally narrows every query to the rows the user may see (implement it on your Policy for row-level security).

type SMTPMailer

type SMTPMailer struct {
	Host     string // "smtp.example.com"
	Port     int    // 587
	Username string
	Password string
	From     string // "Steward <admin@example.com>"
}

SMTPMailer sends mail through a plain SMTP endpoint (STARTTLS when the server offers it, via net/smtp's default behavior).

func (*SMTPMailer) Send

func (m *SMTPMailer) Send(_ context.Context, mail Mail) error

Send implements Mailer.

type Scheduler

type Scheduler interface {
	Add(cronSpec, name string, fn func(context.Context) error) error
	Jobs() []JobInfo
}

Scheduler runs recurring jobs. It is deliberately not wired into the Admin: run it in a worker process (see steward.CLI's worker command) so the panel and background jobs deploy and scale independently.

type SearchDoc

type SearchDoc struct {
	ID     string
	Type   string
	Fields map[string]string
	// Attributes is Fields in the order Searchable declared them, most
	// important first. A map has no order and JSON sorts its keys, so without
	// this an engine that ranks by attribute ranks alphabetically: a match in
	// SubTitle beat a match in Title, which is not what the declaration says.
	Attributes []string
}

SearchDoc is one indexed record. Type is the resource's slug, so one engine can hold every resource in a panel and still answer for one of them.

type SearchHit

type SearchHit struct {
	ID    string
	Type  string
	Score float64
}

SearchHit is one match, in the engine's own order.

type Searcher

type Searcher interface {
	Index(ctx context.Context, docs ...SearchDoc) error
	Delete(ctx context.Context, typ string, ids ...string) error
	Query(ctx context.Context, typ, query string, limit int) ([]SearchHit, error)
}

Searcher backs quick search and the command palette with a full-text engine instead of SQL LIKE. Configure one with Config.Searcher and declare what goes in it with Resource.Searchable.

Index and Delete take batches because a backfill is the normal way an index is first filled, and one round trip per row over a table of any size is not a backfill anyone will finish.

Query returns matching IDs rather than rows. The rows are then read through the repository, so filters, sorts, and the row scope a policy applies all still hold — an engine that knew how to return rows directly would be an engine that had to be taught the panel's authorization, which is not a thing to duplicate.

type Setting

type Setting struct {
	Slug string `gorm:"primaryKey;size:100"`
	// text, not longtext: longtext exists only on MySQL, and this table has to
	// migrate on PostgreSQL and SQLite too. On MySQL text caps a setting at
	// 64KB.
	Value     string `gorm:"type:text"`
	CreatedAt time.Time
	UpdatedAt time.Time
}

Setting is one row of the slug→value KV store.

func (Setting) TableName

func (Setting) TableName() string

type SignedURLStorage

type SignedURLStorage interface {
	SignedURL(ctx context.Context, name string, ttl time.Duration) (string, error)
}

SignedURLStorage is the optional half of Storage: a backend that can hand out a URL good for a limited time, so the bucket behind it never has to be public. An S3-compatible backend implements this with its own presigning; LocalStorage implements it below.

A Storage that does not implement it keeps working — StorageURL falls back to the plain URL, which for LocalStorage is the panel's own authenticated route.

type Sort

type Sort struct {
	Path string
	Desc bool
}

Sort orders results by a field path.

type Storage

type Storage interface {
	Put(ctx context.Context, name string, r io.Reader, size int64, contentType string) (url string, err error)
	Delete(ctx context.Context, name string) error
	URL(name string) string
}

Storage persists uploaded files for File/Image form fields. The default LocalStorage writes below Config.UploadDir; an S3-compatible backend is one small implementation of this interface away.

type Then

type Then struct {
	Action string `json:"action"` // redirect | location | download | refresh | script
	Value  string `json:"value,omitempty"`
}

Then names the client action performed after the toast.

type Widget

type Widget struct {
	// contains filtered or unexported fields
}

Widget is one dashboard tile. Methods chain.

func (*Widget) Color

func (w *Widget) Color(c BadgeColor) *Widget

Color tints a metric tile and its icon, from the panel's colour vocabulary — the same one badges use. An unknown colour is reported by Verify.

func (*Widget) Hint

func (w *Widget) Hint(s string) *Widget

Hint adds secondary text under a metric's value.

func (*Widget) Icon

func (w *Widget) Icon(name string) *Widget

Icon draws a Lucide glyph beside a metric's figure, in the tile's colour. The name is checked at boot.

func (*Widget) Lazy

func (w *Widget) Lazy() *Widget

Lazy defers the widget's data callback to a follow-up request, so a slow aggregate does not hold up the page. The tile renders a skeleton and swaps itself once the fragment arrives.

func (*Widget) Span

func (w *Widget) Span(n int) *Widget

Span sets how many of the grid's three columns the widget occupies. Values outside 1..3 are clamped.

Directories

Path Synopsis
contrib
ginsteward module
s3store module
internal
cron
Package cron parses and evaluates five-field cron expressions at minute resolution, for the worker's scheduler.
Package cron parses and evaluates five-field cron expressions at minute resolution, for the worker's scheduler.
htmlsafe
Package htmlsafe sanitizes untrusted HTML down to an allowlist.
Package htmlsafe sanitizes untrusted HTML down to an allowlist.
httpmatch
Package httpmatch ports dcat-admin's permission matcher: a permission row carries a comma-separated HTTP method list and a newline/comma-separated list of path patterns with * globs; each path line may override the method list with a "GET,POST:" prefix.
Package httpmatch ports dcat-admin's permission matcher: a permission row carries a comma-separated HTTP method list and a newline/comma-separated list of path patterns with * globs; each path line may override the method list with a "GET,POST:" prefix.
migrations
Package migrations holds Steward's embedded framework migrations, applied under the "core" source by Config.AutoMigrate or `migrate up`.
Package migrations holds Steward's embedded framework migrations, applied under the "core" source by Config.AutoMigrate or `migrate up`.
qr
Package qr implements a minimal QR Code encoder: byte mode, error correction level M, versions 1 through 10.
Package qr implements a minimal QR Code encoder: byte mode, error correction level M, versions 1 through 10.
quickdsl
Package quickdsl parses the grid quick-search box's mini query language, ported from dcat-admin:
Package quickdsl parses the grid quick-search box's mini query language, ported from dcat-admin:
ratelimit
Package ratelimit is fixed-window rate limiting, bounding attempts on the token endpoint and the two-factor challenge.
Package ratelimit is fixed-window rate limiting, bounding attempts on the token endpoint and the two-factor challenge.
rules
Package rules implements Steward's declarative field validation: the pipe-separated rule strings a form field carries ("required|max:255| unique:posts,slug,{id}").
Package rules implements Steward's declarative field validation: the pipe-separated rule strings a form field carries ("required|max:255| unique:posts,slug,{id}").
session
Package session implements Steward's stateless session: a JSON payload sealed with AES-256-GCM (key derived from Config.SecretKey) carried in an HttpOnly cookie.
Package session implements Steward's stateless session: a JSON payload sealed with AES-256-GCM (key derived from Config.SecretKey) carried in an HttpOnly cookie.
Package migrate provides a small, versioned database migration runner on top of GORM.
Package migrate provides a small, versioned database migration runner on top of GORM.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL