Lattice Grid

api reference

Layout API Reference

createLayout: windows a reader moves, resizes and closes, the arrangement saved as plain JSON, locking, and the cancellable gestures.

API reference › The dashboard layout

All 29 pages Everything on one page → Developer guide →

The dashboard layout

modules/layout is an opt-in reconfigurable dashboard surface: a cell grid inside an element, and a set of windows placed on it that a user can move, resize and close - by drag or by keyboard. It is the thing a customer would otherwise reach for GridStack or react-grid-layout to get, which means a second dependency, a second sizing model, and a seam where the viewers in it do not resize properly.

It is payload-agnostic, and that is the whole design. A window body is a div with an id. The module creates it, sizes it, and never reads or writes its contents - it does not import createGrid, does not know what a payload is, and never calls into one. What it does instead is emit window:resized with the measured content box, which is the contract. That rule is what keeps the whole module to about 17,500 bytes gzipped (measured on the built bundle: its own code over a shared module runtime of roughly 2KB) and what makes it usable for a payload we have not written yet.

import { createLayout } from '@toclocoinc/lattice-grid/modules/layout';

const layout = createLayout(document.querySelector('#dash'), {
  columns: 12, rows: 8, gap: 8,
  overflowX: 'static', overflowY: 'scroll', rowHeight: '160px',
  windows: [
    { id: 'pipeline', title: 'Pipeline', xPos: 1, yPos: 1, xSize: 6, ySize: 4,
      movable: true, resizable: true, closable: true },
    { id: 'trend', title: 'Trend', xPos: 7, yPos: 1, xSize: 6, ySize: 4,
      movable: true, resizable: true },
  ],
  onWindowResized: ({ id, width, height }) => redraw(id, width, height),
});

// The module made the container; you fill it and you own what is inside it.
createGrid(layout.payload('pipeline'), { rowKey: 'id', rows, columns });

Two independent overflow axes, not one setting. overflowX and overflowY are each 'static' or 'scroll', because a dashboard that scrolls both ways is ordinary and a single enum cannot express it. The difference is what happens to a track's size. A static axis divides the mounted element with minmax(0, 1fr) - never a bare 1fr, whose implicit auto minimum lets one stubborn payload drag a track past the container. A scrolling axis repeats a fixed track (columnWidth / rowHeight) and the canvas extends past the viewport, which then scrolls. That is the owner's "maintaining their sizing", and it is the difference between this and flex-wrap: measured in a real browser, ten 200px columns in a 600px host paint at 200px each over a 2000px canvas, and shrinking the host to 300px leaves the column at 200px and scrolls further.

Spacing takes a real CSS length. gap, padding, columnWidth and rowHeight each accept a number (pixels), or a string: '200px', '25%', '1fr', '2rem', '10vh'. Percentages that sum past 100 are allowed to overflow and scroll rather than being silently scaled down, which is the honest outcome. Anything outside that vocabulary - including calc() and var() - is refused by name with one warning and replaced by the default, because the value is written into an inline style.

Rearrangement. compact: 'vertical' (the default) pushes displaced windows down and then pulls everything up into whatever space that left, so a window dropped into empty space falls to the top of its column - window:moved carries both to (where it was asked to go) and landed (where it actually ended up). compact: 'none' keeps every window exactly where it is put, and enforces non-overlap instead of compacting it away: a resize stops at the nearest neighbour’s edge with the ghost showing the clamped size, a move that would land on a neighbour snaps back, and overlapping placements handed to config.windows or setLayout() are repaired - the later windows move to the next free cells, with one warning naming them. compact: 'horizontal' floats windows left into vacated space instead, so dragging a window out of a row closes the hole sideways rather than leaving it open. It is a third value, not a second pass over the other axis: one gravity direction, never two, because two directions each vacate space the other wants and the arrangement would then depend on which ran last. It settles windows in a canonical left-to-right, then top-to-bottom order, exactly mirroring vertical's top-to-bottom, then left-to-right, so the result depends only on where the windows are and never on the order they were declared or dragged in - the same guarantee vertical has always made, now made on both axes and asserted over all 720 declaration orders of a six-window dashboard on each axis. A value that is none of the three warns once, naming what it was given, and falls back to 'vertical'. The axis a mode compacts along is the axis that can overflow: under 'horizontal' a window that genuinely does not fit the columns settles in an implicit track and can be clipped on a static horizontal axis - exactly as a 'vertical' dashboard can be clipped past its rows on a static vertical axis today. overflowX: 'scroll' is to 'horizontal' what overflowY: 'scroll' is to the default.

Keyboard, to the same standard as the drag. Every movable and resizable window carries a focusable handle running the full grab / move / drop / cancel model the kanban board established: Space or Enter grabs, the arrow keys move a tentative placement, Enter drops it through the same beforeWindowMove gate the pointer drag uses, and Escape cancels. A polite live region announces every step - grabbed, each tentative position with its column and row, dropped, cancelled, and reverted when a handler vetoes the drop - and focus returns to the handle afterwards. A window with chrome: false still gets a handle, because a movable window a keyboard user cannot move is not movable.

An “Edit layout” button, without rebuilding the dashboard. closable, movable and resizable also take a layout-level default, so unlocking a twelve-window dashboard is one setting rather than twenty-four, and setInteractive(true|false|{movable, resizable, closable}) changes that default at runtime - unlock, let the user rearrange, lock again and save getLayout(). Nothing is destroyed and nothing is rebuilt, so every grid, chart and board mounted in a window survives the toggle untouched. The asymmetry is deliberate: you can always take a capability away; you can never grant one where the developer said no. setInteractive(false) locks every window, including one whose own spec says movable: true, so a dashboard hard-locks in a single call without auditing twelve window specs; setInteractive(true) unlocks only the windows that never opted out, so a masthead declared movable: false stays pinned. Both halves of the enforcement move together - the handles a window renders and the checks the pointer and keyboard paths make, because removing a handle stops a mouse while only the gesture check stops a keyboard user already standing on one. config.movable: false and setInteractive(false) are deliberately not the same thing: the config states the default for windows that declare nothing - and false is already that default, so it takes nothing away from a window that declared movable: true - while setInteractive(false) is an active lock that pins every window whatever its own spec says. getInteractive() reports all three states rather than two: undefined where no layout-level default is in force, true, or false for a lock. Reporting “unset” as false would read correctly and round-trip wrongly, so setInteractive(getInteractive()) is a no-op in every state, and a key carrying undefined means “leave this capability alone”. Interactivity is a mode, not part of the arrangement: getLayout() does not carry it, setLayout() does not read it, and no event fires. A locked layout is not a read-only dashboard: the module creates the payload container and never reads or writes its contents, so a grid inside a window is made read-only with the grid's own settings - a dashboard that must not be edited is two decisions, not one.

Which payloads re-lay-out on window:resized, honestly. The grid and the chart each own a ResizeObserver and respond correctly; the Gantt does too since 1.52.0; kanban and KPI do no JS work at all on a resize and need none, because they reflow by CSS construction (a kanban column keeps its 280px and the board starts scrolling). Every one of those is measured in test/layout-browser.test.js rather than asserted, including a grid column declared as a percentage (layout: { width: '50%' }), which follows the window: half of the viewport at 800px, half of it again at 400px.

Idle cost, measured. A twelve-window dashboard is indistinguishable from a page with no layout module on it at all. Over 8 seconds of real Chrome (bench/layout-idle.mjs), twelve windows with empty payloads, twelve independent live grids in them, and a single lone grid with no layout module all sit in the same few-millisecond band - under a tenth of one percent of a core. They are not separated here because they cannot be: seven runs across two machines land between 3.1ms and 9.0ms and the ordering between them inverts run to run, so a stated delta would be reporting the noise floor. The module adds no timer, no frame loop and no polling, and owns exactly one ResizeObserver for the whole layout rather than one per window. The one figure that is a result rather than noise is not this module's: twelve grids derived from one shared parent filtered at 20Hz cost 3,155-3,409ms over the same 8 seconds - several hundred times the quiet band, and stable across every run - because the engine's repaint listener for a derived source's rows:changed calls the renderer directly and bypasses grid.updates.pause(). The bench reports the size of that gap rather than leaving it inferred.

Closing a window does not destroy its payload. window:closed hands the payload container back; whatever you mounted inside it is yours to destroy. Stated plainly because a leaked grid per closed window is the obvious failure, and this module has no way to know that a div contains something with a destroy().

Not in v1: per-frame drag events; and responsive breakpoints - a twelve-window dashboard on a phone is unsolved, and this does not pretend otherwise. Server-side persistence is the host's, with getLayout().

Nesting is not a limitation, because a payload is an ordinary container. A window's payload is a plain div the module creates, sizes and never reads, so anything composes into it exactly as it would into any other element - including another createLayout dashboard, or a modules/tabs strip for tabbed windows. Neither is a special case this module wires up; both are the ordinary consequence of being payload-agnostic, and both are proved by test rather than asserted from the design (test/layout.test.js's two composition tests).

MemberDescription
createLayout(el, config)Create a dashboard layout. columns/rows (default 12/6) divide the element; overflowX/overflowY are each 'static' or 'scroll'; columnWidth/rowHeight are the fixed track sizes a scrolling axis uses; gap (8px), padding (5px) and compact ('vertical', 'horizontal' or 'none', default 'vertical') complete it. A second mount on the same element is refused by name.
config.windows[]Each window: id (required, unique), xPos/yPos/xSize/ySize in 1-based cells (auto-placed in the first free cell when omitted), title, chrome (default true), and closable/movable/resizable/maximisable/minimisable (all default false, so a dashboard the developer wants fixed is fixed without opting out of anything; each also takes a layout-level default of the same name, which a window's own boolean overrides). padding and payloadId (default `${id}-body`) override per window.
payload(id) / window(id) / windows()The payload container for a window - the div carrying its payloadId, which you fill; a copy of a window's current descriptor; every window id in mount order.
add(spec) / move(id, to) / close(id)Add a window after mount (returns its payload container); move or resize one through the same before-events the drag uses; close one through beforeWindowClose. move and close return true/false synchronously with no handler registered, or a Promise<boolean> when a handler deferred.
getLayout() / setLayout(snapshot)The full current arrangement as plain JSON ({columns, rows, windows: [{id, xPos, yPos, xSize, ySize}]}), and its restore. setLayout never throws on garbage, and an entry naming a window that does not exist yet is retained and applied when that window is added.
getState() / setState(snapshot)The versioned persistence pair, following core's and the Gantt's shape: no arguments in, one plain JSON-safe object out, and setState survives whatever is handed to it.
setInteractive(value) / getInteractive()Lock or unlock the whole dashboard at runtime, without destroying it. A boolean sets movable, resizable and closable together; an object sets only the keys it carries, and a key carrying undefined is treated as absent; getInteractive() returns the layout-level values as a copy, three-valued (undefined for unset, true, or false for a lock) so that setInteractive(getInteractive()) is a no-op in every state. The config keys of the same name state the default; only this method takes a capability away. Locking always wins and unlocking never overrides an opt-out: setInteractive(false) pins a window that declared movable: true, and setInteractive(true) leaves a window that declared movable: false pinned. No event fires and getLayout() is unchanged - a mode is not an arrangement. It does not touch maximisable or minimisable either, for the same reason.
maximise(id) / minimise(id) / restore(id)Maximise fills the layout host - the element you mounted on - not the browser window, and hides every other window for the duration. That is deliberate: filling the viewport means position: fixed, whose containing block is the nearest ancestor carrying a transform, filter, contain or will-change, so the same rule fills the screen on one page and lands in a 300px box on the next; filling the host is a geometry change inside the layout and cannot disturb the page around it. Nothing moves: no compaction runs, no placement changes, and the payload container is the same DOM node throughout, so whatever you mounted in it is untouched. Escape restores it from anywhere inside the layout - a focused grid body cell or column heading included - unless something inside has already claimed the key: an open cell editor, a filter menu or a column menu closes first, and the next Escape restores the window. A grid claims only an Escape it actually used, so a maximised grid never keeps the key. Afterwards focus lands on the window's maximise control, so a keyboard user is somewhere they can act rather than wherever the payload left them. minimise(id) draws a window as a single row and hides its payload, keeping the chrome that carries the way back - so the rest of the dashboard closes up around it under whichever compact mode is configured - the windows below pull up under 'vertical', the windows beside it float left under 'horizontal' - which is the point of minimising one. In the arrangement, nothing moves at all: the collapse is a projection of the dashboard, not a change to it, so restore(id) gives back exactly the arrangement that was there - in any order, with any number of other windows still collapsed. All 14,400 minimise/restore orderings of a five-window dashboard are asserted. A window with chrome: false is refused by name: there would be nothing left on screen to restore it with. The controls are opt-in per window (maximisable, minimisable) with a layout-level default of the same name, and setInteractive() does not touch them - a display mode neither moves nor resizes a window in the arrangement, so a locked dashboard can still be blown up to read.
maximised() / minimised()The id of the window filling the host (at most one - maximising a second restores the first), or null; and the ids of every minimised window in mount order. Neither state is part of getLayout(): a mode is not an arrangement, so getLayout() reports the underlying placement in both states - where the window will be when restored - and setLayout() never restores anyone into a mode, moving a minimised window under it instead.
refresh()Re-measure every window and emit window:resized for those that changed. Called automatically; exposed for a host that changed something the module cannot observe, such as revealing an ancestor.
on(name, fn) / off(name, fn)Events: window:moved, window:resized, window:closed, layout:changed; the cancellable beforeWindowMove, beforeWindowResize and beforeWindowClose (call preventDefault(reason?) or return false), each paired with windowMove:cancelled, windowResize:cancelled and windowClose:cancelled. '*' subscribes to every past-tense event and is deliberately never delivered a before-event. Config sugar for all ten. Drag progress is not emitted per frame.
destroy()Stop observing, drop every listener including any left by a gesture in flight, and remove the DOM the module built. Whatever you mounted in a payload is yours to destroy.

Placement, compaction and a saved arrangement, executed

createLayout needs a real host element, the same way createGrid does, so this executed example reaches for the same in-tree DOM test double the suite runs the renderer against headlessly (packages/dom/src/renderer/testdom.js). demo/layout.html is the browser version, with a real grid, chart and KPI rail in three windows that you can drag, resize with the keyboard, close, save and restore.

const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { root } = createTestDom();
const { createLayout } = await import('../packages/modules/layout/index.js');

const layout = createLayout(root, {
  columns: 4, rows: 4,
  windows: [
    { id: 'a', title: 'A', xPos: 1, yPos: 1, xSize: 2, ySize: 1 },
    { id: 'b', title: 'B', xSize: 2, ySize: 1 },   // no coordinates: auto-placed
  ],
});
const shape = () => layout.getLayout().windows.map((w) => `${w.id}@${w.xPos},${w.yPos}`).join(' ');

const placed = shape();                        // B landed in the first free cell: 3,1
const saved = JSON.parse(JSON.stringify(layout.getLayout()));

layout.move('a', { xPos: 3, yPos: 1 });        // drop A on top of B; one layout:changed fires
const pushed = shape();                        // B is pushed down to 3,2

layout.setLayout(saved);                       // the saved arrangement round-trips
const restored = shape();

layout.select('a');                            // programmatic select: rings and focuses A
const sel = layout.selected();                 // read the selection back: 'a'

const payload = layout.payload('a').id;        // the container you fill: 'a-body'
layout.destroy();
return [placed, pushed, restored, payload, sel].join('|');

Type reference

Generated from the type declarations, so it always matches the release. Each surface lists its properties, its methods and the events it raises as three tables; an option or value type lists its members once.

The dashboard layout

LayoutWindow

One window on the cell grid. Deliberately **not** named `WindowSpec`: that name is already taken by the rolling-statistics window (`{ kind: 'count'|'time'|'session', span, size }`) and reusing it would put `kind: 'session'` next to a dashboard pane.

PropertyTypeDescription
idstringA stable, unique id. Required.
xPosnumberThe 1-based column the window starts in. Auto-placed when omitted. (optional)
yPosnumberThe 1-based row the window starts in. Auto-placed when omitted. (optional)
xSizenumberHow many columns it spans (default 1). (optional)
ySizenumberHow many rows it spans (default 1). Ignored when `height` is `'content'`. (optional)
height'content'`'content'` sizes the window's row to what the window holds: the row is as tall as the tallest content-sized window in it (a KPI strip, a form, a grid with `autoHeight`, plain HTML), re-flows when that content changes size, and the rows of fill windows share the remainder. The window spans one row; `ySize` is ignored. Leave it out for a fixed `ySize`. (optional)
minHeightnumber | stringWith `height: 'content'`: the shortest the window may be (px number or CSS length). (optional)
maxHeightnumber | stringWith `height: 'content'`: the tallest the window may be (px number or CSS length); the body scrolls past it. (optional)
titlestringThe title shown in the chrome bar, and the name every control takes. (optional)
chromebooleanWhether to draw the title bar (default `true`). (optional)
closablebooleanWhether to offer a close button (default `false`). (optional)
movablebooleanWhether the window can be moved by drag or keyboard (default `false`). (optional)
resizablebooleanWhether the window can be resized by drag or keyboard (default `false`). (optional)
maximisablebooleanWhether to offer a maximise control in the chrome (default `false`). Maximising fills the **layout host**, not the browser window, and hides every other window for the duration. Escape restores it, unless a payload has already claimed the key. (optional)
minimisablebooleanWhether to offer a minimise control in the chrome (default `false`). A window with `chrome: false` cannot be minimised whatever this says: there would be nothing left on screen to restore it with. (optional)
cornersLayoutCorners 'rounded' | 'square'This window's own corner style; the layout's `corners` default otherwise. (optional)
paddingnumber | stringPadding inside the window; the layout's `padding` (default `'5px'`) otherwise. (optional)
payloadIdstringThe `id` given to the payload container (default `` `${id}-body` ``). (optional)
ariaLabelstringThe window's accessible name, when the title alone is not enough context. (optional)

LayoutInteractive

The three capabilities a layout-level default and `setInteractive()` cover. These are the layout **defaults**, not the per-window resolution: a window that declared `movable: false` stays pinned whatever these say. Three values, not two. `undefined` means no layout-level default is in force and each window's own flag decides; `true` unlocks everything that did not opt out; `false` is an active lock. Reporting `undefined` as `false` would read correctly and round-trip wrongly, so it is reported as it is.

PropertyTypeDescription
movableboolean | undefinedWhether dragging a window to another cell is currently allowed for the layout as a whole. Undefined means nothing has been locked or unlocked, so each window's own flag decides.
resizableboolean | undefinedWhether dragging a window's edge is currently allowed for the layout as a whole, on the same terms as `movable`.
closableboolean | undefinedWhether the close control is currently allowed for the layout as a whole, on the same terms as `movable`.

LayoutSnapshot

The plain, JSON-safe arrangement `getLayout()` returns and `setLayout()` takes.

PropertyTypeDescription
columnsnumberHow many cell columns the layout had when the snapshot was taken. Informational on the way back in: `setLayout` reads only `windows`, and clamps each placement to the layout it is restored into.
rowsnumberHow many cell rows the layout had when the snapshot was taken; informational too.
windows{ id: string; xPos: number; yPos: number; xSize: number; ySize: number; height?: 'content'; minHeight?: number | string; maxHeight?: number | string }[]Each window's id and its cell placement. Position and size only: titles, content and capabilities stay with the configuration.

LayoutPlacement

A cell placement, as carried on the move and resize events.

PropertyTypeDescription
xPosnumberThe window's left-hand column, counted from 1.
yPosnumberThe window's top row, counted from 1.
xSizenumberHow many columns wide the window is; at least 1, and never more than the layout has.
ySizenumberHow many rows tall the window is; at least 1.

LayoutMoveEvent

The payload of `window:moved`, `beforeWindowMove`, `beforeWindowResize`.

PropertyTypeDescription
idstringWhich window moved or was asked to move.
fromLayoutPlacementWhere the window was before the gesture. A move that would change nothing is not reported at all.
toLayoutPlacementWhere the window was asked to go.
landedLayoutPlacementWhere it actually ended up, which under `compact: 'vertical'` may differ. (optional)
originExtract<EventOrigin, 'api' | 'user' | 'init'>Who caused it: `user` for a drag, `api` for a call, `init` for the opening arrangement. Defaults to `user`. (optional)
reasonstring | nullWhy it was cancelled - whatever was passed to `preventDefault`, `prevented` when nothing was, or `error` when a handler threw. Null while nothing has cancelled it. (optional)
defaultPreventedbooleanTrue once a handler has cancelled the action. Read it in a later handler to see that an earlier one already refused. (optional)
MethodSignatureParametersReturnsDescription
preventDefault(reason?: string) => voidreason?: string=> voidCancel the action (only meaningful on a `before*` event). (optional)

LayoutResizeEvent

The payload of `window:resized` - the measured **content box** of the payload container, not a cell count. Emitted when the container genuinely changes size, including on the opening frame; never with a zero box.

PropertyTypeDescription
idstringWhich window changed size.
payloadIdstringThe id of the window's content container, so a host can find the element it mounted into. Defaults to the window id plus `-body`.
payloadHTMLElementThe payload container itself, so a host can act on it directly.
widthnumberThe content box's width in CSS pixels, measured - not a cell count. A change under a pixel is not reported.
heightnumberThe content box's height in CSS pixels, on the same terms as `width`.
xPosnumberThe window's left-hand column at the time of the measurement.
yPosnumberThe window's top row at the time of the measurement.
xSizenumberHow many columns wide the window now is.
ySizenumberHow many rows tall the window now is.

LayoutCloseEvent

The payload of `window:closed` and `beforeWindowClose`.

PropertyTypeDescription
idstringWhich window was closed, or is about to be.
payloadIdstringThe id of its content container - the handle for tearing down whatever was mounted inside.
payloadHTMLElementThe payload container, handed back so the host can destroy what it mounted. (optional)
originViewerOrigin 'api' | 'user'Who caused it: `user` for the close control, `api` for a call. Defaults to `user`. (optional)
reasonstring | nullWhy the close was refused - the reason given to `preventDefault`, `prevented` when none was, or `error` when a handler threw. (optional)
defaultPreventedbooleanTrue once a handler has refused the close. (optional)
MethodSignatureParametersReturnsDescription
preventDefault(reason?: string) => voidreason?: string=> voidRefuse the close, optionally saying why. Only `beforeWindowClose` is cancellable; by `window:closed` the window has gone. (optional)

LayoutWindowMovedEvent

`window:moved`: a window finished moving. The past-tense event, so there is no `preventDefault` on it - the move has happened, and `beforeWindowMove` ({@link LayoutMoveEvent}) is where it could have been stopped.

PropertyTypeDescription
idstringWhich window moved.
fromLayoutPlacementWhere it was before the gesture.
toLayoutPlacementWhere it was asked to go.
landedLayoutPlacementWhere it actually ended up, which under `compact: 'vertical'` may differ from `to`.
originViewerOrigin 'api' | 'user'Who caused it: `user` for a drag or a keyboard move, `api` for `setWindow`.

LayoutWindowClosedEvent

`window:closed`: a window was removed and its payload container handed back. The past-tense event: no `preventDefault`, because the window has gone. The container is **not** destroyed - whatever the host mounted in it is the host's to tear down.

PropertyTypeDescription
idstringWhich window closed.
payloadIdstringThe id of its content container.
payloadHTMLElementThe container itself, handed back so the host can destroy what it mounted.
originViewerOrigin 'api' | 'user'Who caused it: `user` for the close control, `api` for a call.

LayoutMoveCancelledEvent

`windowMove:cancelled` and `windowResize:cancelled`: a `before…` handler refused the gesture. A notification, so it carries no `preventDefault`.

PropertyTypeDescription
idstringWhich window did not move.
fromLayoutPlacementWhere it is, and stays.
toLayoutPlacementWhere it would have gone.
originViewerOrigin 'api' | 'user'Who asked for the gesture that was refused: `user` for a drag or keyboard move, `api` for `setWindow`.
reasonstringThe reason given to `preventDefault`, or `'prevented'`.

LayoutCloseCancelledEvent

`windowClose:cancelled`: a `beforeWindowClose` handler refused the close. A notification, so it carries no `preventDefault`.

PropertyTypeDescription
idstringWhich window stayed open.
payloadIdstringThe id of its content container.
originViewerOrigin 'api' | 'user'Who asked for the close that was refused.
reasonstringThe reason given to `preventDefault`, or `'prevented'`.

LayoutEventPayloads

What a handler receives, per layout event.

PropertyTypeDescription
window:movedLayoutWindowMovedEventWhich window moved, from where, to where, and where it landed.
window:resizedLayoutResizeEventThe measured content box of the window's payload container.
window:closedLayoutWindowClosedEventThe window that closed, and the container handed back.
layout:changedLayoutChangedEventThe arrangement, and what settled it.
beforeWindowMoveLayoutMoveEventThe move about to be applied, with `preventDefault` to stop it.
beforeWindowResizeLayoutMoveEventThe resize about to be applied, with `preventDefault` to stop it.
beforeWindowCloseLayoutCloseEventThe close about to happen, with `preventDefault` to refuse it.
windowMove:cancelledLayoutMoveCancelledEventThe move that was refused, and why.
windowResize:cancelledLayoutMoveCancelledEventThe resize that was refused, and why.
windowClose:cancelledLayoutCloseCancelledEventThe close that was refused, and why.

LayoutChangedEvent

The payload of `layout:changed`: the whole arrangement, plus what moved it.

PropertyTypeDescription
causestringWhat moved the arrangement: `move`, `resize`, `close`, `add`, `minimise`, `restore`, `setLayout` or `init`.

LayoutConfig

Dashboard layout configuration.

PropertyTypeDescription
columnsnumberCell columns across the mounted element (default 12). (optional)
rowsnumberCell rows down the mounted element (default 6). (optional)
overflowXLayoutOverflow 'static' | 'scroll'Horizontal overflow (default `'static'`). (optional)
overflowYLayoutOverflow 'static' | 'scroll'Vertical overflow (default `'static'`). (optional)
columnWidthnumber | stringFixed column track size, used only when `overflowX` is `'scroll'` (default `'240px'`). (optional)
rowHeightnumber | stringFixed row track size, used only when `overflowY` is `'scroll'` (default `'160px'`). (optional)
gapnumber | stringThe gap between cells (default `'8px'`). (optional)
stackBelownumberHost width in px below which the windows stack one per row, full width and in reading order, each keeping its own height (default `0`: never). Above it the configured arrangement returns; `getLayout()` always reports the configured placements. (optional)
paddingnumber | stringThe default padding inside a window (default `'5px'`). (optional)
compactLayoutCompaction 'vertical' | 'horizontal' | 'none'Rearrangement (default `'vertical'`). One gravity direction, never two: `'vertical'` pushes displaced windows down and then floats everything up, `'horizontal'` pushes them right and then floats everything left - so dragging a window out of a row closes the hole sideways - and `'none'` leaves every placement exactly where it was put: a resize stops at the nearest neighbour's edge and a move snaps back instead of overlapping, and overlapping placements given to `config.windows` or `setLayout()` are repaired to the next free cells with a warning. An unrecognised value warns once, naming what it got, and falls back to `'vertical'`. (optional)
cornersLayoutCorners 'rounded' | 'square'The default corner style for every window that does not declare its own (default `'rounded'`, today's look). `'square'` zeroes the radius on the window, its title bar, the minimised strip and the maximised frame. The rounded radius itself is themed via `--lattice-layout-radius`, separately from the shared `--lattice-radius` every other module reads. A window's own `corners` overrides this. An unrecognised value warns once and falls back to `'rounded'`. (optional)
movablebooleanThe default `movable` for every window that does not declare its own (default `false`). This states a default, so `false` takes nothing away from a window that declared `movable: true`; `setInteractive(false)` is the active lock that does. (optional)
resizablebooleanThe default `resizable` for windows that declare none (default `false`); see `movable`. (optional)
closablebooleanThe default `closable` for windows that declare none (default `false`); see `movable`. (optional)
maximisablebooleanThe default `maximisable` for windows that declare none (default `false`). Not touched by `setInteractive()`: a display mode neither moves nor resizes a window in the arrangement, so a locked dashboard can still be blown up to read. (optional)
minimisablebooleanThe default `minimisable` for windows that declare none (default `false`); see `maximisable`. (optional)
windowsLayoutWindow[]The windows, in mount order. (optional)
layoutLayoutSnapshotAn arrangement to apply at mount, as produced by `getLayout()`. (optional)
ariaLabelstringThe layout region's accessible name. (optional)
theme'light' | 'dark' | 'auto' | (string & {})The theme for the whole dashboard, written to `data-theme` on the layout's root: `'dark'` darkens the window chrome, the window bodies and every grid, chart, KPI panel, tab strip, kanban board and gantt mounted in a window that does not set a theme of its own. `'light'` forces light inside a dark page; `'auto'` follows `prefers-color-scheme`. Unset follows the nearest `data-theme` on an ancestor, and is light when there is none. (optional)
messages{ t(key: string, params?: Record<string, unknown>): string }A message catalogue, e.g. `grid.messages`; built-in English seeds otherwise. (optional)
MethodSignatureParametersReturnsDescription
onWindowMoved(event: LayoutMoveEvent) => voidevent: LayoutMoveEvent=> voidCalled after a window has moved, alongside the `window:moved` event. (optional)
onWindowResized(event: LayoutResizeEvent) => voidevent: LayoutResizeEvent=> voidCalled when a window's measured content box changes, alongside `window:resized` - including on the opening frame. (optional)
onWindowClosed(event: LayoutCloseEvent) => voidevent: LayoutCloseEvent=> voidCalled after a window has closed, with its content container handed back so the host can destroy what it mounted. Alongside `window:closed`. (optional)
onLayoutChanged(event: LayoutChangedEvent) => voidevent: LayoutChangedEvent=> voidCalled whenever the arrangement settles, with the whole snapshot and what caused it - the hook for persisting a dashboard. Alongside `layout:changed`. (optional)
onBeforeWindowMove(event: LayoutMoveEvent) => boolean | void | Promise<boolean>event: LayoutMoveEvent=> boolean | void | Promise<boolean>Called before a move is applied. Return `false`, call `preventDefault(reason)`, or throw, to refuse it; return a promise and the move waits for it. Alongside the `beforeWindowMove` event. (optional)
onBeforeWindowResize(event: LayoutMoveEvent) => boolean | void | Promise<boolean>event: LayoutMoveEvent=> boolean | void | Promise<boolean>Called before a resize is applied, on the same terms as `onBeforeWindowMove`. (optional)
onBeforeWindowClose(event: LayoutCloseEvent) => boolean | void | Promise<boolean>event: LayoutCloseEvent=> boolean | void | Promise<boolean>Called before a window closes, on the same terms as `onBeforeWindowMove`. This is where an unsaved-changes prompt belongs. (optional)
onWindowMoveCancelled(event: LayoutMoveEvent) => voidevent: LayoutMoveEvent=> voidCalled when a move was refused, with the reason on the payload. Alongside `windowMove:cancelled`. (optional)
onWindowResizeCancelled(event: LayoutMoveEvent) => voidevent: LayoutMoveEvent=> voidCalled when a resize was refused. Alongside `windowResize:cancelled`. (optional)
onWindowCloseCancelled(event: LayoutCloseEvent) => voidevent: LayoutCloseEvent=> voidCalled when a close was refused. Alongside `windowClose:cancelled`. (optional)

Layout

A reconfigurable dashboard: a cell grid inside an element, and a set of windows on it that a user can move, resize and close by pointer or by keyboard. The module is **payload-agnostic**: a window body is a container with an id, which this module creates and sizes and never reads. It tells a payload it was resized by emitting `window:resized`; it never calls into one, because it cannot know what one is.

Properties
PropertyTypeDescription
elHTMLElementThe element the layout was mounted on. It carries the layout's host class, which is also how a second `createLayout` on the same element is refused. (read-only)
readyPromise<void>Resolves once every tile is built and sized and every Lattice view the host mounted into a tile at creation has its own first full render in the DOM and measurable - aggregated from each mounted view's `ready`. Rejects if the layout is destroyed first. See also {@link whenRendered}.
Methods
MethodSignatureParametersReturnsDescription
windows(): string[] - string[]The window ids, in mount order.
payload(id: string): HTMLElement | nullid: stringHTMLElement | nullThe payload container for a window, or `null`.
window(id: string): LayoutWindow | nullid: stringLayoutWindow | nullA copy of one window's current descriptor, or `null`.
add(spec: LayoutWindow): HTMLElementspec: LayoutWindowHTMLElementAdd a window after mount; returns its payload container.
move(id: string, to: Partial<LayoutPlacement>): boolean | Promise<boolean>id: string
to: Partial<LayoutPlacement>
boolean | Promise<boolean>Move or resize a window, through the same before-events the drag uses.
close(id: string): boolean | Promise<boolean>id: stringboolean | Promise<boolean>Close a window through `beforeWindowClose`; the payload is not destroyed.
maximise(id: string): booleanid: stringbooleanBlow one window up to fill the layout host, hiding the rest. It fills the **host element**, not the browser window, so there is no `position: fixed` (whose containing block is the nearest ancestor carrying a `transform` or a `contain`, which is why the same rule fills the screen on one page and lands in a 300px box on the next), no reparenting and nothing that can disturb the page around the dashboard. **Nothing moves**: no compaction runs, no placement changes, and the payload container is the same DOM node throughout. **Escape restores it**, from anywhere inside the layout - a focused grid body cell or column heading included - unless a payload has already claimed the key: an open cell editor, filter menu or column menu closes first, and the next Escape restores the window. Afterwards focus lands on the window's maximise control. A minimised window is expanded first, and maximising a second window restores the first.
minimise(id: string): booleanid: stringbooleanCollapse one window to a single row: its payload is hidden and its chrome stays, carrying the control that brings it back. On screen it becomes one row and the windows below pull up into the space under `compact: 'vertical'`. In the arrangement nothing moves at all - the collapse is a projection of it - so `restore()` gives back exactly the arrangement that was there, in **any** order and with any number of other windows still collapsed. A window with `chrome: false` is refused, with a warning naming it.
restore(id: string): booleanid: stringbooleanLeave whichever display mode a window is in; `false` when it was in none.
select(id: string | null): string | nullid: string | nullstring | nullSelect a window, or clear the selection with `null` - the programmatic half of a selection a host draws its own chrome around (the Designer's properties panel). At most one is selected; selecting a second clears the first. The selected frame carries `data-selected="true"`, is ringed with the accent token and focused, and the id now selected (or `null`) is returned. A selected window that is closed is deselected.
selected(): string | null - string | nullThe id of the currently selected window, or `null`.
maximised(): string | null - string | nullThe id of the window filling the host, or `null`. At most one.
minimised(): string[] - string[]The ids of every currently minimised window, in mount order.
getLayout(): LayoutSnapshot - LayoutSnapshotThe full current arrangement. **A mode is not an arrangement**: this reports the *underlying* placement of a maximised or minimised window - where it will be when restored - never the geometry it is drawn at.
setLayout(incoming: LayoutSnapshot | LayoutWindow[]): numberincoming: LayoutSnapshot | LayoutWindow[]numberRestore an arrangement; never throws on garbage.
getState(): { version: number; layout: LayoutSnapshot } - { version: number; layout: LayoutSnapshot }A versioned snapshot, following core's and gantt's shape.
setState(snapshot: unknown): numbersnapshot: unknownnumberRestore a `getState()` snapshot; never throws on garbage.
setInteractive(value: boolean | Partial<LayoutInteractive>): LayoutInteractivevalue: boolean | Partial<LayoutInteractive>LayoutInteractiveLock or unlock the dashboard at runtime - the "Edit layout" button. A boolean sets all three capabilities; an object sets only the keys it carries. Nothing is destroyed, so every payload survives the toggle. The asymmetry is deliberate: **you can always take a capability away; you can never grant one where the developer said no.** `setInteractive(false)` locks every window, including one whose own spec says `movable: true`; `setInteractive(true)` unlocks only the windows that never opted out. `config.movable: false` and `setInteractive(false)` are deliberately not the same thing: the config states the *default* for windows that declare nothing (and `false` is already that default, so it takes nothing away from a window that opted in), while this is an *active lock*. A key carrying `undefined` is treated as absent, so `setInteractive(getInteractive())` is a no-op in every state. A locked layout is not a read-only dashboard: this module never reads or writes a payload, so a grid inside a window is made read-only with the grid's own settings.
getInteractive(): LayoutInteractive - LayoutInteractiveThe layout-level interactivity now in force, as a copy - `undefined` where no layout-level default is set, so the result round-trips through `setInteractive`.
refresh(): number - numberRe-measure every window and emit `window:resized` for those that changed.
on( name: LayoutEventName | '*', fn: (event: LayoutEventPayloads[LayoutEventName]) => unknown, ): () => voidname: LayoutEventName | '*'
fn: (event: LayoutEventPayloads[LayoutEventName]) => unknown,
() => voidSubscribe to a layout event, or to `'*'` for every past-tense one; returns a function that unsubscribes. Only an explicit `before…` subscription can cancel an action - the `'*'` stream never gates. What each event carries is {@link LayoutEventPayloads}; the handler is declared with the widest of them, so narrow on the name inside it.
off(name: LayoutEventName | '*', fn: (event: LayoutEventPayloads[LayoutEventName]) => unknown): voidname: LayoutEventName | '*'
fn: (event: LayoutEventPayloads[LayoutEventName]) => unknown
voidRemove a handler registered with `on`.
destroy(): void - voidTear the layout down; whatever the host mounted in a payload is the host's to destroy.
Events
EventWhenPayloadCancellable
window:movedA window finished moving, with where it was asked to go and where it actually landed.LayoutWindowMovedEventno
window:resizedA window's payload container changed size, measured in CSS pixels - including on the opening frame.LayoutResizeEventno
window:closedA window was closed and its payload container handed back.LayoutWindowClosedEventno
layout:changedThe arrangement settled after an add, close, move, resize, minimise, restore or `setLayout`.LayoutChangedEventno
beforeWindowMoveA window is about to move; cancellable.LayoutMoveEventyes
beforeWindowResizeA window is about to be resized by a drag or a call; cancellable.LayoutMoveEventyes
beforeWindowCloseA window is about to be closed; cancellable.LayoutCloseEventyes
windowMove:cancelledA `beforeWindowMove` handler refused the move.LayoutMoveCancelledEventno
windowResize:cancelledA `beforeWindowResize` handler refused the resize.LayoutMoveCancelledEventno
windowClose:cancelledA `beforeWindowClose` handler refused the close.LayoutCloseCancelledEventno