layoutmgr

package module
v0.0.0-...-b02f92a Latest Latest
Warning

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

Go to latest
Published: Jun 2, 2026 License: MIT Imports: 25 Imported by: 0

README

Iridium Layout Manager

A standalone plugin for Iridium that lets your end-users arrange their own dashboard pages: pick blocks (Iridium widgets or arbitrary templ components) from a dropdown, drop them onto the page, drag-to-reorder, resize their column span, and save the result. Layouts persist per-user (cookie/session by default; pluggable for DB-backed storage).

Ported from the FilamentPHP Filament Layout Manager plugin to Iridium's HTMX + Alpine + templ stack. The port was done essentially 100% by claude/codex partly as an exercise to see how well it handles direct ports.

Drag-to-reorder is powered by the Alpine Sort plugin that Iridium already bundles, so the plugin ships only a tiny JS helper and a CSS file.

This plugin can serve as a starting point for understanding how to build your own Iridium plugin. The beauty of Go is its duck typing lets you mix and match your custom code into Iridium fairly easily.

Install

go get github.com/asosick/iridium_layout_manager@latest

Quick start

package main

import (
    "github.com/iridiumgo/iridium/core/panel"
    "github.com/iridiumgo/iridium/core/widget/stats"

    layoutmgr "github.com/asosick/iridium_layout_manager"
    "myapp/dashboard/components" // your own templ components
)

func main() {
    salesStats := stats.NewStatsWidgetResolvable("sales-stats").
        SetName("Sales Today")
        // ... configure stats ...

    dashboard := layoutmgr.NewLayoutManagerPage("Dashboard", "dashboard").
        Blocks(
            layoutmgr.StaticBlock("welcome", "Welcome banner", components.Welcome()),
            layoutmgr.DynamicBlock("greeting", "Personal greeting",
                func(w http.ResponseWriter, r *http.Request) templ.Component {
                    return components.Hello(r) // can read the current user
                },
            ),
            layoutmgr.WidgetBlock("salesStats", "Sales stats", salesStats),
        ).
        GridColumns(3)

    p := panel.NewPanel("/admin").Pages(dashboard)
    // ... mount the panel as usual ...
}

That's it. Visit the page, click 🔒 Edit, pick a block from the dropdown, hit + Add — your block appears on the grid. Drag the handle to reorder, use the +// buttons to resize, and × to remove. Click Save to commit the current arrangement (fires your SaveHook, or just persists the session by default).

Block kinds

Adapter Use when
StaticBlock(key, label, c) Tile is a fixed templ.Component (no per-request data).
DynamicBlock(key, label, fn) Tile needs the (w, r) (DB queries, current user, etc.).
WidgetBlock(key, label, w) Tile is an existing Iridium widget (chart, stats, form, table).

You can mix them freely in a single Blocks(...) call.

Configuration

layoutmgr.NewLayoutManagerPage("Dashboard", "dashboard").
    Blocks(...).
    GridColumns(3).            // 1..N column grid
    LayoutCount(3).            // number of pages the user can flip between (default 3)
    Heading("My Dashboard").   // override the H1
    ShowLockButton(true).      // hide to lock the page in edit mode permanently
    Reorderable(true).         // enable drag-to-reorder
    Resizeable(true).          // enable +/-/full-width controls
    SaveHook(myDBSave).        // optional — persist to DB on Save click
    LoadHook(myDBLoad)         // optional — load from DB on each render
Multiple pages

Each user gets LayoutCount separate pages (default 3) to arrange independently. Numbered buttons at the top switch between them, and cmd/ctrl + 1..9 jump straight to a page. In edit mode every page number is shown so you can populate empty ones; in view mode only pages that actually contain blocks show their number (and the strip hides entirely when only one page is in use). All pages persist together in a single LayoutState.

Persistence

By default the plugin stores each user's layout in Iridium's gorilla session (under a key namespaced by the page slug, separate from the auth cookie so clearing one doesn't nuke the other). Zero config required.

To swap in your own storage, provide both hooks:

type LoadHook func(r *http.Request) (LayoutState, error)
type SaveHook func(r *http.Request, state LayoutState) error

LayoutState is JSON-serialisable; just persist it as a blob keyed by user.

Note: Working changes (add / remove / resize / reorder) are always committed to the session so the user's edits survive page reloads. Your SaveHook is only called when the user clicks the Save button — that's the "commit to permanent storage" event.

How drag-to-reorder works

The plugin uses the Alpine Sort plugin that Iridium's core JS bundle already ships with — no extra JS dependency. After a drop:

  1. Alpine reorders the <div data-lm-block> elements in the DOM.
  2. The handleReorder Alpine method (registered by the plugin) reads the new order from data-id attributes.
  3. It POSTs {"order": [...]} to the page's /lm/reorder endpoint.
  4. The server reorders the saved state and returns the freshly-rendered grid; htmx swaps it back in.

The server's order is the source of truth, so the morph-swap also corrects any DOM drift if a concurrent request lands.

Status

v1 — single layout, session-backed by default, no per-block custom state. Multi-layout tabs and per-block stores are deferred features from the Filament original; both can land later without API breaks.

License

MIT

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

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Block

type Block struct {
	ID   string `json:"id"`
	Type string `json:"type"`
	Cols int    `json:"cols"`
}

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

func StaticBlock(key, label string, c templ.Component) BlockSpec

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

func (l *Layout) Find(id string) (*Block, int)

Find returns the block with the given ID and its index within this layout, or (nil, -1) if not present.

func (*Layout) Remove

func (l *Layout) Remove(id string) bool

Remove drops the block with the given ID. Returns true if anything was removed.

func (*Layout) Reorder

func (l *Layout) Reorder(order []string) bool

Reorder reshuffles the blocks to match the given ID order. Unknown IDs are ignored; blocks present in the layout but missing from order are dropped to the end in their original relative order. Returns true if the order changed.

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

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

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.

type SaveHook

type SaveHook func(r *http.Request, state LayoutState) error

SaveHook persists the LayoutState for the current request.

Directories

Path Synopsis
templ: version: v0.3.1001
templ: version: v0.3.1001

Jump to

Keyboard shortcuts

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