Documentation
¶
Overview ¶
Package app is the URL → rendered page pipeline for GoFastr UI.
It composes the screen registry (Router), the per-screen lifecycle (Screen + Load/Render), the shared chrome (Layout), and the request- in-context helpers into a single App value. Anything that turns "request for URL X" into "rendered HTML for URL X" lives in this package.
The DI container is its own concern and lives in the sibling core-ui/di package. App wires one in via App.Container so screens can be injected during the Load phase. Visual primitives live in core-ui/html (1:1 HTML tags) and core-ui/patterns (higher- level UI patterns).
Quick Start ¶
Components can implement ScreenTitler, ScreenDescriber, and ScreenTyper individually, or the combined ScreenSpec interface. Register detects each interface independently — implement only what you need:
type Home struct{}
func (h *Home) Render() render.HTML { return render.Text("hi") }
func (h *Home) ScreenTitle() string { return "Home" }
// That's it — ScreenType defaults to ScreenPage, description defaults to empty.
application := app.NewApp("MyApp")
application.Register("/", &Home{}, nil)
html, _ := application.RenderPage(ctx, "/")
For full control, implement ScreenSpec (all three methods). For components without any metadata interfaces, use RegisterScreen with the builder: app.RegisterScreen(app.NewScreen("/", comp).WithTitle("Home"), nil).
Screen Types ¶
Four screen types are supported:
- Page: full-page views rendered inside <main>
- Drawer: side panels rendered inside <aside>
- Sheet: bottom sheets rendered as modal dialogs
- Dialog: modal dialogs with overlay backdrop
Layouts ¶
Layouts provide shared chrome (header, sidebar, footer) that wraps screen content. A default layout can be set for all screens, or individual screens can override with their own layout.
Dependency Injection ¶
App.Provide / App.Inject are thin convenience wrappers over the core-ui/di.Container held in App.Container. Register constructors or values with Provide, then resolve them with Resolve or inject them into struct fields tagged with `inject:""`.
check-csp:ignore-file This file builds regex patterns that match (and strip) <script> and <style> blocks from rendered HTML before converting it to Markdown for /llm.md. The patterns never emit script tags — they only consume them — but the literal `<script` substring trips the no-inline-script linter. The directive exempts this file from that check.
Index ¶
- func AppLLMMD(a *App) string
- func AppLLMMDHandler(a *App) http.Handler
- func ComposeLayouts(innermost *ScreenGroup, content render.HTML) render.HTML
- func QueryFromContext(ctx context.Context) url.Values
- func RequestFromContext(ctx context.Context) *http.Request
- func ScreenLLMMD(screen *Screen) string
- func ScreenLLMMDHandler(screen *Screen) http.Handler
- func ValidateScreenOutput(screen *Screen, output string) []string
- func WithRequest(ctx context.Context, r *http.Request) context.Context
- type App
- func (a *App) Inject(target any) error
- func (a *App) Provide(constructor any) error
- func (a *App) Register(path string, comp component.Component, layout *Layout)
- func (a *App) RegisterScreen(screen *Screen, layout *Layout)
- func (a *App) RenderPage(ctx context.Context, path string) (render.HTML, error)
- func (a *App) RenderPageResult(ctx context.Context, path string) (RenderResult, error)
- func (a *App) RenderPartial(ctx context.Context, path string) (render.HTML, error)
- func (a *App) RenderPartialResult(ctx context.Context, path string) (RenderResult, error)
- func (a *App) RenderScreenRaw(path string) (render.HTML, error)
- func (a *App) Routes() []RouteEntry
- func (a *App) SetDefaultLayout(layout *Layout)
- func (a *App) WithTheme(theme style.Theme) *App
- type Decision
- type DecisionKind
- type Layout
- func (l *Layout) WithFooter(c component.Component) *Layout
- func (l *Layout) WithHeader(c component.Component) *Layout
- func (l *Layout) WithSidebar(c component.Component) *Layout
- func (l *Layout) Wrap(content render.HTML) render.HTML
- func (l *Layout) WrapCtx(ctx context.Context, content render.HTML) render.HTML
- func (l *Layout) WrapNested(content render.HTML) render.HTML
- func (l *Layout) WrapNestedCtx(ctx context.Context, content render.HTML) render.HTML
- type ParamSetter
- type Policy
- type PolicyFunc
- type RenderResult
- type RouteEntry
- type Router
- func (r *Router) DefaultLayout(layout *Layout)
- func (r *Router) GetDefaultLayout() *Layout
- func (r *Router) Paths() []string
- func (r *Router) RenderRaw(path string) (render.HTML, error)
- func (r *Router) Resolve(path string) (*Screen, map[string]string, bool)
- func (r *Router) Screen(screen *Screen, layout *Layout)
- func (r *Router) ScreenGroup(sg *ScreenGroup)
- func (r *Router) Screens() map[string]*Screen
- type Screen
- type ScreenActions
- type ScreenComponentID
- type ScreenDescriber
- type ScreenGroup
- func (g *ScreenGroup) AllScreens() []*Screen
- func (g *ScreenGroup) Layout() *Layout
- func (g *ScreenGroup) Prefix() string
- func (g *ScreenGroup) RenderLayout(content render.HTML) render.HTML
- func (g *ScreenGroup) RenderLayoutCtx(ctx context.Context, content render.HTML) render.HTML
- func (g *ScreenGroup) Screen(screen *Screen, layout *Layout)
- func (g *ScreenGroup) Screens() []*Screen
- func (g *ScreenGroup) SubGroup(prefix string, layout *Layout, policies ...Policy) *ScreenGroup
- func (g *ScreenGroup) WithPolicy(p Policy) *ScreenGroup
- type ScreenLoader
- type ScreenSpec
- type ScreenTitler
- type ScreenType
- type ScreenTyper
- type StaticComponent
- type StaticPathsProvider
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AppLLMMDHandler ¶
AppLLMMDHandler returns an http.Handler that serves the top-level page index markdown for all screens in the app.
func ComposeLayouts ¶
func ComposeLayouts(innermost *ScreenGroup, content render.HTML) render.HTML
ComposeLayouts walks from the innermost group to the outermost, wrapping content in each group's layout. Outer groups wrap inner groups. The innermost content (the screen) is wrapped first by its immediate group, then by each parent group going outward.
func QueryFromContext ¶
QueryFromContext is a convenience that returns the URL query Values of the request in ctx, or an empty Values when no request is attached (e.g. SSG builds).
func RequestFromContext ¶
RequestFromContext returns the *http.Request associated with ctx, or nil if none was set. Always nil-check the result — Load may run in build-time SSG too, where there is no live request.
func ScreenLLMMD ¶
ScreenLLMMD generates an LLM-friendly markdown document for a single screen/page. The output describes the route, its parameters, the screen type, and any metadata useful for LLM agents navigating the app.
When a screen implements ScreenDescriber, the description is included. When it implements StaticPathsProvider, the SSG behavior is documented. When it implements ScreenActions, the server-action contract is noted.
func ScreenLLMMDHandler ¶
ScreenLLMMDHandler returns an http.Handler that serves the LLM-friendly markdown for a single screen. Content-Type is text/markdown.
func ValidateScreenOutput ¶
ValidateScreenOutput checks a screen's rendered output for common mistakes and returns a slice of warning strings. An empty slice means no issues. This is intended for dev-mode linting — it does not affect production rendering.
Currently checks:
- Nested <main> elements: ScreenPage screens are already wrapped in <main> by the framework, so including <main> in the component output creates nested <main> elements (invalid HTML).
Types ¶
type App ¶
type App struct {
// Name is the application name, used in the page title.
Name string
// Container is the dependency injection container.
Container *di.Container
// Router maps paths to screens and layouts.
Router *Router
// Theme holds optional theme configuration (can be nil).
Theme *style.Theme
// NoLLMMD disables auto-generated llm.md for all pages in this app.
NoLLMMD bool
}
App is the root of the UI hierarchy. It holds the DI container, theme, router, and global configuration.
func (*App) Register ¶
Register adds a screen to the app by reading metadata from the component if it implements ScreenTitler, ScreenDescriber, or ScreenTyper. This is the preferred registration API — the component declares its own metadata.
application.Register("/", &HomeScreen{}) // HomeScreen implements ScreenTitler
If the component does not implement ScreenTyper, it defaults to ScreenPage. If it does not implement ScreenTitler, the title defaults to empty. If it does not implement ScreenDescriber, the description defaults to empty.
func (*App) RegisterScreen ¶
RegisterScreen adds a screen to the app's router.
func (*App) RenderPage ¶
RenderPage renders a full HTML page (<!DOCTYPE html><html>...) for a screen path. It resolves the route, evaluates the screen's policy chain, locks the screen for concurrent param safety, injects route params, runs DI, calls Load(ctx), and finally renders.
RenderPage is the simple entry point: it returns HTML for Allow and RenderAlt decisions, and an error for Redirect/Block (which cannot be expressed as HTML). Use RenderPageResult when you need to react to all four DecisionKinds.
func (*App) RenderPageResult ¶
RenderPageResult is the policy-aware variant of RenderPage. It resolves the screen, evaluates the effective Policy chain, and returns a RenderResult describing the outcome.
- DecisionAllow: HTML holds the full <!DOCTYPE>… document.
- DecisionRedirect: URL is the destination; HTML is empty.
- DecisionRenderAlt: the alt component took the place of the screen's component; HTML holds the full document.
- DecisionBlock: Status holds the HTTP status code; HTML is empty.
func (*App) RenderPartial ¶
RenderPartial returns just the screen content (no layout, no <html>/<head>/<body>). Used for client-side navigation where the layout is already in the DOM. Runs the same param-injection / DI / Load pipeline as RenderPage, including policy evaluation. Returns an error for Redirect/Block decisions — partials cannot express those; use RenderPartialResult instead.
func (*App) RenderPartialResult ¶
RenderPartialResult is the policy-aware variant of RenderPartial. Same semantics as RenderPageResult but returns just the content fragment, suitable for client-side navigation swaps.
func (*App) RenderScreenRaw ¶
RenderScreenRaw is a policy-bypassing convenience over Router.RenderRaw. INTENDED FOR INTERNAL/SSG USE ONLY — HTTP handlers must use RenderPageResult so the Policy chain is honored (auth gating, RenderAlt, Redirect, Block).
func (*App) Routes ¶
func (a *App) Routes() []RouteEntry
Routes returns all registered screen paths as RouteEntry slices.
func (*App) SetDefaultLayout ¶
SetDefaultLayout sets the default layout.
func (*App) WithTheme ¶
WithTheme sets the application theme and returns the app for chaining. Auto-fills missing token Names from struct-field paths (Colors.Primary → "primary", Colors.PrimaryFg → "primary-fg"), then validates — passing a partially-populated theme (e.g. a Color with empty Value) panics with the field path naming the missing piece. This catches "silently broken styling" failures at startup, not at the first page render.
type Decision ¶
type Decision struct {
Kind DecisionKind
URL string // populated for DecisionRedirect
AltFactory func() component.Component // populated for DecisionRenderAlt — called PER REQUEST
Status int // populated for DecisionBlock
Message string // optional, for DecisionBlock
}
Decision is the outcome of Policy.Decide. Construct with the helper functions (Allow, Redirect, RenderAlt, Block) — the struct shape is public only so hosts can inspect it.
type DecisionKind ¶
type DecisionKind int
DecisionKind classifies the outcome of evaluating a Policy.
const ( // DecisionAllow lets the request proceed to normal Load+Render. DecisionAllow DecisionKind = iota // DecisionRedirect sends an HTTP 303 (or equivalent) to URL. DecisionRedirect // DecisionRenderAlt swaps Alt in for the screen's own component // before Load+Render run. The original screen's Load is skipped; // Alt's Load runs if Alt implements ScreenLoader. DecisionRenderAlt // DecisionBlock aborts with the given HTTP Status and optional // Message. Use for hard 401/403/404 outcomes. DecisionBlock )
type Layout ¶
type Layout struct {
// Name identifies the layout (used in CSS class).
Name string
// Header is an optional header component rendered with role="banner".
Header component.Component
// Sidebar is an optional sidebar component rendered as navigation.
Sidebar component.Component
Footer component.Component
}
Layout defines shared chrome that wraps screens. A layout has slots for header, sidebar, footer, and content.
func (*Layout) WithFooter ¶
WithFooter sets the layout footer and returns the layout for chaining.
func (*Layout) WithHeader ¶
WithHeader sets the layout header and returns the layout for chaining.
func (*Layout) WithSidebar ¶
WithSidebar sets the layout sidebar and returns the layout for chaining.
func (*Layout) Wrap ¶
Wrap renders the layout as the OUTERMOST shell: its content region is the page's single <main id="main-content">. If the layout is nil, Wrap returns the content unchanged. Chrome renders with a background context; use WrapCtx to give context-aware chrome the request context.
func (*Layout) WrapCtx ¶
WrapCtx is Wrap with an explicit context, threaded into the chrome (header/sidebar/footer) so a ContextComponent in any slot renders with the live request context (auth-aware nav, current tenant, etc.).
func (*Layout) WrapNested ¶
WrapNested renders the layout as an INNER shell — one composed inside another layout's <main> (e.g. a screen-group layout nested in the app's default layout). It contributes its sidebar + content region but NOT a <main> landmark, so the page keeps exactly one <main id="main-content"> instead of emitting a duplicate (invalid id + a second landmark).
type ParamSetter ¶
ParamSetter is implemented by components that accept route parameters before rendering. The app calls SetParams after resolving a dynamic route.
type Policy ¶
Policy decides what to do with a request before screen render. Implementations must be stateless and side-effect-free; the resolver may call Decide multiple times during a request and never holds onto the returned Decision past the response.
func EffectivePolicies ¶
EffectivePolicies returns the full policy chain for s: walk parent ScreenGroups outermost→innermost, then the screen's own policies. Hosts rarely need to call this directly — use ResolvePolicy.
type PolicyFunc ¶
PolicyFunc adapts a plain function to the Policy interface.
type RenderResult ¶
type RenderResult struct {
Kind DecisionKind
HTML render.HTML // populated for DecisionAllow and DecisionRenderAlt
URL string // populated for DecisionRedirect
Status int // populated for DecisionBlock
Message string // optional, for DecisionBlock
}
RenderResult is the outcome of resolving and rendering a page or partial. HTTP hosts inspect Kind first to choose between writing the HTML body, issuing a redirect, or returning an error status.
type RouteEntry ¶
RouteEntry describes a registered route for consumption by the DevServer.
type Router ¶
type Router struct {
// contains filtered or unexported fields
}
Router maps paths to screens and layouts. Supports both exact paths ("/about") and dynamic patterns ("/products/:slug").
func (*Router) DefaultLayout ¶
DefaultLayout sets the default layout for screens without one.
func (*Router) GetDefaultLayout ¶
GetDefaultLayout returns the layout currently set as the default for screens that don't declare one. May return nil. Read-only accessor — uihost's not-found path wraps a synthesized 404 component in this layout so the error page shares chrome with the rest of the site.
func (*Router) RenderRaw ¶
RenderRaw renders a screen by path with no policy resolution and no request context. INTENDED FOR INTERNAL/SSG USE ONLY — callers in HTTP-serving code should use App.RenderPageResult, which evaluates the Policy chain before invoking Load and Render.
func (*Router) Resolve ¶
Resolve finds the screen for a given path. Returns the screen, the extracted route params (for dynamic routes), and whether it was found. Params are returned separately to avoid mutating the shared screen instance.
func (*Router) Screen ¶
Screen registers a screen with an optional layout. If layout is nil, the screen will use the default layout (if set). Paths with ":param" segments are registered as dynamic routes.
func (*Router) ScreenGroup ¶
func (r *Router) ScreenGroup(sg *ScreenGroup)
ScreenGroup registers all screens from a ScreenGroup into the router. Each screen in the group (and its sub-groups) is registered with the router, and the group's layout is applied.
When the runtime navigates between siblings in the same group, only the inner content is swapped — the layout shell is DOM-stable.
type Screen ¶
type Screen struct {
// Path is the route pattern, e.g., "/users/:id".
Path string
// Name is a human-readable name for the screen.
Name string
// Title is the page title for <title> and route graph.
Title string
// Description is a short description for route preloading metadata.
Description string
// Type classifies the screen as page, drawer, sheet, or dialog.
Type ScreenType
// Component is the component that renders this screen.
Component component.Component
// Layout is an optional layout override for this screen.
Layout *Layout
// NoLLMMD disables auto-generated /path/llm.md for this screen.
NoLLMMD bool
// contains filtered or unexported fields
}
Screen represents a top-level view in the app hierarchy.
func (*Screen) PolicyChain ¶
PolicyChain returns a copy of the screen's own policy chain (does not include inherited group policies — use EffectivePolicies for the full walk). Returns nil when no policies are attached.
func (*Screen) Render ¶
Render renders the screen's component with appropriate ARIA landmarks. Equivalent to RenderCtx(context.Background()) — use RenderCtx when a request context is available so ContextComponent screens receive it.
func (*Screen) RenderCtx ¶
RenderCtx renders the screen's component with ARIA landmarks, passing ctx through to ContextComponent implementations.
func (*Screen) WithDescription ¶
WithDescription sets the screen's description.
func (*Screen) WithPolicy ¶
WithPolicy appends a Policy to the screen's own chain. The screen's chain is evaluated after any parent ScreenGroup policies; the first non-Allow Decision wins. Call multiple times to add several policies.
type ScreenActions ¶
type ScreenActions interface {
Actions()
}
ScreenActions is an optional interface for screens that declare server actions. If implemented, the host auto-compiles actions when the screen is registered. The Actions method uses component.On() to register handlers, same as InteractiveComponent.
type ScreenComponentID ¶
type ScreenComponentID interface {
ScreenSpec
// ComponentID returns the ID used for action registration and data-component.
ComponentID() string
}
ScreenComponentID is an optional extension of ScreenSpec that lets a screen declare its component ID for action compilation. If not implemented, the ID is derived from the route path. The component ID must match the data-component attribute in the rendered HTML.
type ScreenDescriber ¶
type ScreenDescriber interface {
ScreenDescription() string
}
ScreenDescriber is an optional interface for components that declare their own page description (used for route preloading metadata and SEO).
type ScreenGroup ¶
type ScreenGroup struct {
// contains filtered or unexported fields
}
ScreenGroup defines a shared layout that wraps every child screen. Screen groups nest: navigating between siblings inside the same group swaps only the inner content region, not the layout shell.
Usage:
sidebar := app.NewStaticComponent(sidebarHTML)
group := app.NewScreenGroup("/settings", app.NewLayout("settings").WithSidebar(sidebar))
group.Screen(screen1, nil)
group.Screen(screen2, nil)
appRouter.ScreenGroup(group)
func NewScreenGroup ¶
func NewScreenGroup(prefix string, layout *Layout, policies ...Policy) *ScreenGroup
NewScreenGroup creates a screen group with the given prefix and layout. The prefix is the URL path prefix shared by all screens in the group. The layout wraps every child screen's content.
Optional policies attach a chain of access policies to every screen in the group (and sub-groups). The chain is evaluated outermost group → innermost group → screen at request time; the first non- Allow Decision wins. Use this to gate entire route prefixes:
dash := app.NewScreenGroup("/dashboard", dashLayout, auth.SessionPolicy())
dash.Screen(app.NewScreen("/", &HomeScreen{}), nil) // inherits SessionPolicy
dash.Screen(app.NewScreen("/billing", &BillingScreen{}).
WithPolicy(auth.RolePolicy("admin")), nil) // SessionPolicy + RolePolicy
func (*ScreenGroup) AllScreens ¶
func (g *ScreenGroup) AllScreens() []*Screen
AllScreens returns all screens in this group and its descendants.
func (*ScreenGroup) Layout ¶
func (g *ScreenGroup) Layout() *Layout
Layout returns the layout for this group.
func (*ScreenGroup) Prefix ¶
func (g *ScreenGroup) Prefix() string
Prefix returns the URL prefix for this group.
func (*ScreenGroup) RenderLayout ¶
func (g *ScreenGroup) RenderLayout(content render.HTML) render.HTML
RenderLayout wraps content in the group's layout. If the group has no layout, returns content unchanged.
The rendered wrapper carries a data-fui-screen-group attribute so the runtime knows this is a layout boundary that should be preserved during sibling-screen navigation.
func (*ScreenGroup) RenderLayoutCtx ¶
RenderLayoutCtx is RenderLayout with the request context threaded into the group layout's chrome.
func (*ScreenGroup) Screen ¶
func (g *ScreenGroup) Screen(screen *Screen, layout *Layout)
Screen registers a screen within this group. The screen's path is resolved relative to the group's prefix. If the screen has no layout, the group's layout is applied.
The screen path can be:
- Absolute ("/users") — used as-is, but must start with the group prefix
- Relative ("users") — prefixed with the group's prefix
func (*ScreenGroup) Screens ¶
func (g *ScreenGroup) Screens() []*Screen
Screens returns all screens registered directly in this group (not including sub-groups).
func (*ScreenGroup) SubGroup ¶
func (g *ScreenGroup) SubGroup(prefix string, layout *Layout, policies ...Policy) *ScreenGroup
SubGroup creates a nested screen group. The child group's prefix is resolved relative to this group's prefix. The child inherits this group's layout unless it declares its own.
Optional policies are appended to the parent's policy chain at resolution time — they don't replace inherited policies. A child's screens are gated by parent.policies + child.policies + screen.Policies in that order.
func (*ScreenGroup) WithPolicy ¶
func (g *ScreenGroup) WithPolicy(p Policy) *ScreenGroup
WithPolicy appends a Policy to the group's chain. Returns the group for chaining. Chain is evaluated before any per-screen policies.
type ScreenLoader ¶
ScreenLoader is an optional interface for screens that need to fetch data before rendering. Load runs AFTER route params have been injected and AFTER DI services are wired, but BEFORE Render. Mutating fields on the screen is the expected pattern — those fields are then read by Render.
The same Load runs for both SSR (per-request) and SSG (at build time), so implementations should be deterministic given the same context inputs. Network calls are fine; non-deterministic state (random IDs, time-of-day branches) will produce different output between SSG runs.
type ScreenSpec ¶
type ScreenSpec interface {
ScreenTitler
ScreenDescriber
ScreenTyper
}
ScreenSpec is a convenience interface that groups ScreenTitler, ScreenDescriber, and ScreenTyper. Components can implement the full interface, or implement just the individual interfaces they need. For example, a component that only needs ScreenTitle can implement ScreenTitler alone — ScreenType defaults to ScreenPage and description defaults to empty.
The builder methods (WithTitle, WithDescription, WithScreenType) still work as overrides when using RegisterScreen instead of Register.
type ScreenTitler ¶
type ScreenTitler interface {
ScreenTitle() string
}
ScreenTitler is an optional interface for components that declare their own page title. The returned string is the page-specific portion — the framework appends the app name from AppConfig.Name to produce the full <title>. For example, ScreenTitle() returning "Dashboard" with AppConfig.Name "MyApp" produces <title>Dashboard — MyApp</title>.
type ScreenType ¶
type ScreenType int
ScreenType classifies the kind of screen.
const ( // ScreenPage is a full-page view. ScreenPage ScreenType = iota // ScreenDrawer is a side panel. ScreenDrawer // ScreenSheet is a bottom sheet. ScreenSheet // ScreenDialog is a modal dialog. ScreenDialog )
func (ScreenType) String ¶
func (t ScreenType) String() string
String returns a human-readable description of the screen type.
type ScreenTyper ¶
type ScreenTyper interface {
ScreenType() ScreenType
}
ScreenTyper is an optional interface for components that declare their screen type. If not implemented, the screen defaults to ScreenPage. Most screens can omit this — only implement it for drawers, sheets, or dialogs.
type StaticComponent ¶
StaticComponent wraps raw HTML as a component.Component. Useful for layout regions that don't change per-request (sidebars, headers).
func NewStaticComponent ¶
func NewStaticComponent(h render.HTML) *StaticComponent
NewStaticComponent creates a component from raw HTML.
func (*StaticComponent) Render ¶
func (s *StaticComponent) Render() render.HTML
Render returns the pre-rendered HTML.
type StaticPathsProvider ¶
StaticPathsProvider is an optional interface for screens that mount on a dynamic route pattern (e.g. "/products/:slug") and want to participate in static-site generation. The returned slice is one entry per concrete URL the SSG build should produce; each entry maps each ":param" in the route pattern to the value used for that build.
func (s *ProductDetailScreen) StaticPaths(ctx context.Context) []map[string]string {
return []map[string]string{
{"slug": "device"},
{"slug": "gadget"},
}
}
Routes whose screen does not implement StaticPathsProvider are skipped at build time (they remain reachable via SSR if the server is running).