Documentation
¶
Overview ¶
Package layoutmgr is an Iridium plugin that lets end-users arrange their own dashboards: pick blocks (widgets or arbitrary templ components) from a dropdown, drop them onto the page, drag to reorder, and resize their column span. Layouts are persisted per-user (session by default; pluggable via Save/Load hooks) so they survive across requests.
This is a Go port of the FilamentPHP "Filament Layout Manager" plugin (https://github.com/asosick/filament-layout-manager), adapted to Iridium's HTMX + Alpine + templ stack. The Alpine sort plugin (already bundled by iridium-core) handles drag-to-reorder, so the plugin ships only a tiny JS helper and a CSS file.
Basic usage:
layoutPage := layoutmgr.NewLayoutManagerPage("Dashboard", "dashboard").
Blocks(
layoutmgr.StaticBlock("welcome", "Welcome", welcomeBlock()),
layoutmgr.WidgetBlock("salesChart", "Sales Chart", salesChartWidget),
).
GridColumns(3)
panel.Pages(layoutPage)
Index ¶
- type Block
- type BlockSpec
- type Layout
- type LayoutManagerPage
- func (p *LayoutManagerPage) Blocks(blocks ...BlockSpec) *LayoutManagerPage
- func (p *LayoutManagerPage) CanAccess(fn func(ctx *ctxPanel.PanelPage) bool) *LayoutManagerPage
- func (p *LayoutManagerPage) ContentSize(value enum.PageSize) *LayoutManagerPage
- func (p *LayoutManagerPage) ContentSizeFn(fn func(ctx *ctxPanel.PanelPage) enum.PageSize) *LayoutManagerPage
- func (p *LayoutManagerPage) Description(value string) *LayoutManagerPage
- func (p *LayoutManagerPage) DescriptionFn(fn func(ctx *ctxPanel.PanelPage) string) *LayoutManagerPage
- func (p *LayoutManagerPage) FooterColumns(columns map[string]int) *LayoutManagerPage
- func (p *LayoutManagerPage) FooterColumnsFixed(columns int) *LayoutManagerPage
- func (p *LayoutManagerPage) FooterWidgets(widgets ...widget.IWidgetResolvable) *LayoutManagerPage
- func (p *LayoutManagerPage) GetComponent(w http.ResponseWriter, r *http.Request) (templ.Component, error)
- func (p *LayoutManagerPage) GridColumns(n int) *LayoutManagerPage
- func (p *LayoutManagerPage) HasBreadCrumbs() *LayoutManagerPage
- func (p *LayoutManagerPage) HasBreadCrumbsFn(fn func(ctx *ctxPanel.PanelPage) bool) *LayoutManagerPage
- func (p *LayoutManagerPage) HeaderActions(acts ...actions.IActionResolvable[any]) *LayoutManagerPage
- func (p *LayoutManagerPage) HeaderColumns(columns map[string]int) *LayoutManagerPage
- func (p *LayoutManagerPage) HeaderColumnsFixed(columns int) *LayoutManagerPage
- func (p *LayoutManagerPage) HeaderWidgets(widgets ...widget.IWidgetResolvable) *LayoutManagerPage
- func (p *LayoutManagerPage) Heading(h string) *LayoutManagerPage
- func (p *LayoutManagerPage) LayoutCount(n int) *LayoutManagerPage
- func (p *LayoutManagerPage) LoadHook(fn LoadHook) *LayoutManagerPage
- func (p *LayoutManagerPage) NavigationGroup(group string) *LayoutManagerPage
- func (p *LayoutManagerPage) NavigationHidden() *LayoutManagerPage
- func (p *LayoutManagerPage) NavigationIcon(ic *icon.Icon) *LayoutManagerPage
- func (p *LayoutManagerPage) NavigationLabel(label string) *LayoutManagerPage
- func (p *LayoutManagerPage) NavigationOrder(order int) *LayoutManagerPage
- func (p *LayoutManagerPage) NavigationTabHidden() *LayoutManagerPage
- func (p *LayoutManagerPage) RegisterRoutes(mux wrapper.IMux)
- func (p *LayoutManagerPage) Reorderable(enabled bool) *LayoutManagerPage
- func (p *LayoutManagerPage) Resizeable(enabled bool) *LayoutManagerPage
- func (p *LayoutManagerPage) SaveHook(fn SaveHook) *LayoutManagerPage
- func (p *LayoutManagerPage) ShowLockButton(show bool) *LayoutManagerPage
- func (p *LayoutManagerPage) SkipAuth() *LayoutManagerPage
- func (p *LayoutManagerPage) SkipPanel() *LayoutManagerPage
- func (p *LayoutManagerPage) Title(value string) *LayoutManagerPage
- func (p *LayoutManagerPage) TitleFn(fn func(ctx *ctxPanel.PanelPage) string) *LayoutManagerPage
- type LayoutState
- func (s *LayoutState) ContentFlags(count int) []bool
- func (s *LayoutState) EnsureLayouts(count int)
- func (s *LayoutState) FirstUsedLayout(count int) int
- func (s *LayoutState) LayoutAt(i, count int) *Layout
- func (s *LayoutState) UnmarshalJSON(data []byte) error
- func (s *LayoutState) UsedLayouts(count int) int
- type LoadHook
- type RouteRegistrar
- type SaveHook
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Block ¶
Block is one placed instance inside the user's layout.
- ID is a stable per-instance identifier (UUID) so the front-end can target a specific block for resize/remove without confusing it with siblings of the same Type.
- Type matches a BlockSpec.Key() — the LayoutManagerPage looks up the spec by Type to render the block.
- Cols is the column span this instance takes inside the layout grid, clamped to [1, GridColumns].
type BlockSpec ¶
type BlockSpec interface {
// Key uniquely identifies this block type. Stored in the layout state so
// the server knows which BlockSpec to re-render each instance with.
Key() string
// Label is the human-readable name shown in the "Add" dropdown.
Label() string
// Render produces the templ.Component for ONE instance of this block, with
// access to the current request (so the component can hit the database,
// read the authed user, etc.).
Render(w http.ResponseWriter, r *http.Request) templ.Component
// RegisterRoutes is called once at boot, scoped under the page's mux, so
// blocks that own HTTP routes (e.g. a widget that paginates its data) can
// register them. Most blocks will leave this empty.
RegisterRoutes(mux RouteRegistrar)
}
BlockSpec describes a block the user can add to the layout grid. The LayoutManagerPage is configured with a set of BlockSpecs and the user picks among them (by Label) in the "Add" dropdown.
Implementations come in two flavours out of the box:
- Block(key, label, c) wraps a static templ.Component. Use this for arbitrary dashboard tiles.
- WidgetBlock(key, label, w) adapts an iridium widget resolvable (the same chart / stats / form / table widgets you'd use in any other page).
func DynamicBlock ¶
func DynamicBlock(key, label string, render func(w http.ResponseWriter, r *http.Request) templ.Component) BlockSpec
DynamicBlock is like Block but renders via a per-request function — letting the block read the request (e.g. fetch the current user's data).
func StaticBlock ¶
StaticBlock wraps a static templ.Component as a BlockSpec — the simplest possible adapter. Use this when your tile is purely presentational (no per-request data needed). For data-driven tiles, use DynamicBlock or WidgetBlock.
StaticBlock("welcome", "Welcome", components.Welcome())
func WidgetBlock ¶
func WidgetBlock(key, label string, w widget.IWidgetResolvable) BlockSpec
WidgetBlock adapts an iridium widget resolvable (chart / stats / form / table) into a BlockSpec. The widget's routes are registered under the page's mux automatically.
type Layout ¶
type Layout struct {
Blocks []Block `json:"blocks"`
}
Layout is a single customizable page (a "layout" / "view" in the Filament original). Each layout holds its own ordered list of blocks. Users flip between layouts with the numbered selector buttons / cmd+N hotkeys.
func (*Layout) Find ¶
Find returns the block with the given ID and its index within this layout, or (nil, -1) if not present.
type LayoutManagerPage ¶
type LayoutManagerPage struct {
*panel.CustomPanelPage
// contains filtered or unexported fields
}
LayoutManagerPage is an Iridium panel page that lets the end-user arrange dashboard blocks (widgets or arbitrary templ components). It's a drop-in page — register it with your panel like any other:
panel.Pages(layoutmgr.NewLayoutManagerPage("Dashboard", "dashboard").
Blocks(...).GridColumns(3))
Persistence defaults to the iridium session store, so layouts survive across requests with zero configuration. Pass SaveHook/LoadHook to plug your own database.
func NewLayoutManagerPage ¶
func NewLayoutManagerPage(name, slug string) *LayoutManagerPage
NewLayoutManagerPage constructs a layout-manager page rooted at the given slug. Until you add at least one Block(...) the "Add" dropdown will be empty — see Blocks().
Defaults:
- GridColumns = 2
- ShowLockButton = true
- Reorderable = true
- Resizeable = true
- Persistence = per-user session
func (*LayoutManagerPage) Blocks ¶
func (p *LayoutManagerPage) Blocks(blocks ...BlockSpec) *LayoutManagerPage
Blocks registers the BlockSpecs the user can pick from in the "Add" dropdown. Order is preserved.
func (*LayoutManagerPage) CanAccess ¶
func (p *LayoutManagerPage) CanAccess(fn func(ctx *ctxPanel.PanelPage) bool) *LayoutManagerPage
CanAccess installs an auth gate that runs before the page renders.
func (*LayoutManagerPage) ContentSize ¶
func (p *LayoutManagerPage) ContentSize(value enum.PageSize) *LayoutManagerPage
ContentSize sets the page content width (see enum.PageSize).
func (*LayoutManagerPage) ContentSizeFn ¶
func (p *LayoutManagerPage) ContentSizeFn(fn func(ctx *ctxPanel.PanelPage) enum.PageSize) *LayoutManagerPage
ContentSizeFn is a callback alias for ContentSize.
func (*LayoutManagerPage) Description ¶
func (p *LayoutManagerPage) Description(value string) *LayoutManagerPage
Description sets the descriptive subtitle shown beneath the title.
func (*LayoutManagerPage) DescriptionFn ¶
func (p *LayoutManagerPage) DescriptionFn(fn func(ctx *ctxPanel.PanelPage) string) *LayoutManagerPage
DescriptionFn is a callback alias for Description.
func (*LayoutManagerPage) FooterColumns ¶
func (p *LayoutManagerPage) FooterColumns(columns map[string]int) *LayoutManagerPage
FooterColumns sets the per-breakpoint grid column count for footer widgets.
func (*LayoutManagerPage) FooterColumnsFixed ¶
func (p *LayoutManagerPage) FooterColumnsFixed(columns int) *LayoutManagerPage
FooterColumnsFixed sets the same footer column count at every breakpoint.
func (*LayoutManagerPage) FooterWidgets ¶
func (p *LayoutManagerPage) FooterWidgets(widgets ...widget.IWidgetResolvable) *LayoutManagerPage
FooterWidgets sets widgets rendered in the page footer (below the grid).
func (*LayoutManagerPage) GetComponent ¶
func (p *LayoutManagerPage) GetComponent(w http.ResponseWriter, r *http.Request) (templ.Component, error)
GetComponent is called per request to render the page. We construct a fresh per-request CustomPanelPage snapshot with our dynamically-built content (state-aware) and let iridium-core's chrome resolver wrap it in the panel. Each call gets its own snapshot so concurrent requests don't race on ContentObj.
func (*LayoutManagerPage) GridColumns ¶
func (p *LayoutManagerPage) GridColumns(n int) *LayoutManagerPage
GridColumns sets the max columns in the underlying grid (default 2). Block instances can span 1..GridColumns columns each via resize.
func (*LayoutManagerPage) HasBreadCrumbs ¶
func (p *LayoutManagerPage) HasBreadCrumbs() *LayoutManagerPage
HasBreadCrumbs enables breadcrumbs for the page.
func (*LayoutManagerPage) HasBreadCrumbsFn ¶
func (p *LayoutManagerPage) HasBreadCrumbsFn(fn func(ctx *ctxPanel.PanelPage) bool) *LayoutManagerPage
HasBreadCrumbsFn is a callback alias for HasBreadCrumbs.
func (*LayoutManagerPage) HeaderActions ¶
func (p *LayoutManagerPage) HeaderActions(acts ...actions.IActionResolvable[any]) *LayoutManagerPage
HeaderActions sets actions rendered on the right of the page header.
func (*LayoutManagerPage) HeaderColumns ¶
func (p *LayoutManagerPage) HeaderColumns(columns map[string]int) *LayoutManagerPage
HeaderColumns sets the per-breakpoint grid column count for header widgets.
func (*LayoutManagerPage) HeaderColumnsFixed ¶
func (p *LayoutManagerPage) HeaderColumnsFixed(columns int) *LayoutManagerPage
HeaderColumnsFixed sets the same header column count at every breakpoint.
func (*LayoutManagerPage) HeaderWidgets ¶
func (p *LayoutManagerPage) HeaderWidgets(widgets ...widget.IWidgetResolvable) *LayoutManagerPage
HeaderWidgets sets widgets rendered in the page header (above the grid).
func (*LayoutManagerPage) Heading ¶
func (p *LayoutManagerPage) Heading(h string) *LayoutManagerPage
Heading overrides the page title shown at the top of the page (defaults to the page name). Alias for Title — kept for back-compat.
func (*LayoutManagerPage) LayoutCount ¶
func (p *LayoutManagerPage) LayoutCount(n int) *LayoutManagerPage
LayoutCount sets how many separate pages (layouts) the user can customize and flip between with the numbered selector buttons / cmd+N hotkeys (default 3). Values < 1 are clamped to 1 (single-page behaviour).
func (*LayoutManagerPage) LoadHook ¶
func (p *LayoutManagerPage) LoadHook(fn LoadHook) *LayoutManagerPage
LoadHook plugs in custom retrieval (mirror of SaveHook). Called on each page render and on each mutation to read the current state.
func (*LayoutManagerPage) NavigationGroup ¶
func (p *LayoutManagerPage) NavigationGroup(group string) *LayoutManagerPage
NavigationGroup places this page under a named group in the sidebar.
func (*LayoutManagerPage) NavigationHidden ¶
func (p *LayoutManagerPage) NavigationHidden() *LayoutManagerPage
NavigationHidden hides this page from the sidebar (still reachable + shown in the sub-page tab strip).
func (*LayoutManagerPage) NavigationIcon ¶
func (p *LayoutManagerPage) NavigationIcon(ic *icon.Icon) *LayoutManagerPage
NavigationIcon sets the sidebar icon for this page.
func (*LayoutManagerPage) NavigationLabel ¶
func (p *LayoutManagerPage) NavigationLabel(label string) *LayoutManagerPage
NavigationLabel overrides the label shown in the sidebar (defaults to the page name).
func (*LayoutManagerPage) NavigationOrder ¶
func (p *LayoutManagerPage) NavigationOrder(order int) *LayoutManagerPage
NavigationOrder pins the page's sidebar position (lower = earlier).
func (*LayoutManagerPage) NavigationTabHidden ¶
func (p *LayoutManagerPage) NavigationTabHidden() *LayoutManagerPage
NavigationTabHidden hides this page from the sub-page tab strip (still shown in the sidebar).
func (*LayoutManagerPage) RegisterRoutes ¶
func (p *LayoutManagerPage) RegisterRoutes(mux wrapper.IMux)
RegisterRoutes registers the main page route (delegated to BasePage) plus the plugin's own htmx endpoints (add/remove/resize/reorder/save) under the page's scoped mux. We override the embedded CustomPanelPage.RegisterRoutes so the page handler uses *our* GetComponent (the embedded type's RegisterRoutes captures its own method, missing our dynamic-content override).
Widget routes are registered exactly the way iridium's PanelPageResolvable does it (see panel_page.go RegisterWidgets): each widget's slug is baked with the page slug, its Actionable trait is hung off the page's Carrier so nested action routes (table modals, search/filter endpoints, row actions) get registered, and routes go on the parent-scoped mux (NOT pre-prefixed with the page slug) since the slug is now part of the widget's identity. Skipping any one of those steps breaks modals / search / filters — which is why table widgets were previously broken.
func (*LayoutManagerPage) Reorderable ¶
func (p *LayoutManagerPage) Reorderable(enabled bool) *LayoutManagerPage
Reorderable enables drag-to-reorder of blocks in edit mode (default true).
func (*LayoutManagerPage) Resizeable ¶
func (p *LayoutManagerPage) Resizeable(enabled bool) *LayoutManagerPage
Resizeable enables +/- column-span controls in edit mode (default true).
func (*LayoutManagerPage) SaveHook ¶
func (p *LayoutManagerPage) SaveHook(fn SaveHook) *LayoutManagerPage
SaveHook plugs in custom persistence (e.g. write to your database). The hook is called whenever the user clicks the Save button. Returning an error surfaces a notification to the user.
func (*LayoutManagerPage) ShowLockButton ¶
func (p *LayoutManagerPage) ShowLockButton(show bool) *LayoutManagerPage
ShowLockButton toggles the lock/unlock control that flips between view and edit modes (default true). When hidden, the page is permanently in edit mode.
func (*LayoutManagerPage) SkipAuth ¶
func (p *LayoutManagerPage) SkipAuth() *LayoutManagerPage
SkipAuth skips the auth middleware defined on your panel.
func (*LayoutManagerPage) SkipPanel ¶
func (p *LayoutManagerPage) SkipPanel() *LayoutManagerPage
SkipPanel skips the panel middleware defined on your panel.
func (*LayoutManagerPage) Title ¶
func (p *LayoutManagerPage) Title(value string) *LayoutManagerPage
Title sets the page title shown at the top of the page (defaults to name).
func (*LayoutManagerPage) TitleFn ¶
func (p *LayoutManagerPage) TitleFn(fn func(ctx *ctxPanel.PanelPage) string) *LayoutManagerPage
TitleFn is a callback alias for Title.
type LayoutState ¶
type LayoutState struct {
Layouts []Layout `json:"layouts"`
}
LayoutState is the full serialized state for one user. It holds one or more Layouts (pages). v1 stored a flat block list under "blocks"; that legacy shape is migrated transparently into Layouts[0] on load (see UnmarshalJSON).
func (*LayoutState) ContentFlags ¶
func (s *LayoutState) ContentFlags(count int) []bool
ContentFlags returns a per-index bool slice of length count, true where the layout at that index has at least one block. Drives the "only show numbers for pages that have content" behaviour in view mode.
func (*LayoutState) EnsureLayouts ¶
func (s *LayoutState) EnsureLayouts(count int)
EnsureLayouts pads the Layouts slice so it has at least count entries. Used so a user can target any layout index in [0, count) even before they've added anything to it.
func (*LayoutState) FirstUsedLayout ¶
func (s *LayoutState) FirstUsedLayout(count int) int
FirstUsedLayout returns the index of the first layout that holds content, or 0 if every layout is empty. Used to focus a sensible default page on load.
func (*LayoutState) LayoutAt ¶
func (s *LayoutState) LayoutAt(i, count int) *Layout
LayoutAt returns a pointer to the layout at index i (creating intermediate layouts as needed up to count). An out-of-range index clamps to 0.
func (*LayoutState) UnmarshalJSON ¶
func (s *LayoutState) UnmarshalJSON(data []byte) error
UnmarshalJSON decodes the current ({"layouts":[{"blocks":[...]}]}) shape and transparently migrates the legacy single-layout shape ({"blocks":[...]}) into Layouts[0], so users who saved a layout before multi-page support keep it.
func (*LayoutState) UsedLayouts ¶
func (s *LayoutState) UsedLayouts(count int) int
UsedLayouts counts how many layouts (capped at count) hold at least one block. The selector strip stays hidden in view mode unless this is > 1.
type LoadHook ¶
type LoadHook func(r *http.Request) (LayoutState, error)
LoadHook fetches the LayoutState for the current request. Return a zero LayoutState{} (not nil) when nothing is stored yet.
type RouteRegistrar ¶
type RouteRegistrar interface {
HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))
}
RouteRegistrar is the subset of an HTTP mux a block needs to register routes. It exists so the plugin doesn't drag iridium's internal mux interface into every block author's import graph.