Skip to content
D
Documentation

Layout — Grid Pack

reference
9 min readUpdated

Import these from @grafloria/engine.

Classes

GridPackEngine

ts
class GridPackEngine

Properties

NameTypeDefaultDescription
floatboolean
maxRows?numberRow bound (see GridPackOptions.maxRows). Undefined = unbounded. Changed through {@link setBound}.
capacity?numberRow capacity (see GridPackOptions.capacity). Undefined = unbounded.

Methods

  • get columns(): number — Live column count. Change it only through {@link setColumns}.
  • constructor(items: GridPackItem[] = [], options: GridPackOptions = {})
  • getItems(): readonly GridPackItem[]
  • getItem(id: string): GridPackItem | undefined
  • rows(): number — Content height in rows: max(y+h) over all items (0 when empty).
  • hasOverlaps(): boolean — True when any two items overlap — the invariant every op must preserve.
  • add(item: GridPackItem): GridPackItem | null — Add an item. An explicit legal position is honoured verbatim. When the position collides — or the item asks for autoPosition — the tile AUTO-POSITIONS: a row-major scan for the first hole it fits, which is gridstack's autoPosition and NOT the same thing as gravity (gravity climbs one column; a hole at (3,0) under an occupied column is only reachable by the scan — the spec's first red proved it).
  • remove(id: string): void
  • beginGesture(): void — Begin a drag/resize gesture: fresh displaced-tile memory, and a full snapshot so cancelGesture (Escape) can restore every tile (gridstack saveInitial / restoreInitial).
  • endGesture(): void — End a gesture: memory does NOT outlive it (E2, and the S4 swap lock).
  • cancelGesture(): void — Escape: restore every tile to its gesture-start cell AND size, bring back the tiles removed during the gesture, drop the ones added, and restore the bound. Surviving tiles keep their object identity (a binder holds them).
  • setBound(rows: number): boolean — Change the row bound — a container asking for rows on behalf of a child (tile first, step 1). Refused below the current content. Inside a gesture the change rides in the snapshot, so Escape gives the rows back.
  • moveCheck(id: string, x: number, y: number, options: MoveCheckOptions = {}): GridPackResult — Try to put id at cell (x,y). Applies the full pipeline on acceptance: swap (three shapes) | push-down (+skip below locked) → settle (teleport memory + gravity). Refuses: out-of-gesture no-ops, cells intersecting a locked tile (E4b), and collisions under the anti-jitter coverage gate. { gate: false } (see {@link MoveCheckOptions}) is first-placement mode: gate and swaps are skipped, push-down still applies, E4b still refuses.
  • resizeCheck(id: string, w: number, h: number, options: ResizeCheckOptions = {}): GridPackResult — Resize id to w×h cells. Growth is CLAMPED so the tile never covers a locked tile (E4b applied to size); displaced neighbours push + settle, and return when the size shrinks back (E1/S2).
  • placeBeside(id: string, neighbourId: string, side: BesideSide, row?: number): PlaceBesideResult — Put id NEXT TO neighbourId on side, at row (the pointer's row, clamped to the neighbour's rows; the neighbour's own row by default) — the one sideways primitive the tile-first drag model needs (step 1).

With room on that side the mover simply takes the cell. At the board's edge the neighbour SHIFTS over by the mover's span and the mover takes the edge — the fluid demo's side panel sits at the right edge and "after it" was nowhere. With no room even shifted (a full-width neighbour), the mover takes the cell and the neighbour goes DOWN under it, the way any tile gives way. Top always pushes; bottom always places.

The mover steps off the board while the neighbour shifts: its own cell must not be what refuses the shift (a chart as wide as the panel, lab L94). Locked tiles refuse as everywhere; a bound rollback names itself.

  • setColumns(next: number, layout: GridColumnLayout = 'moveScale'): boolean — Change the board's COLUMN COUNT — gridstack's column(n, layout), and the whole of responsive behaviour in one operation.

Three things happen, in this order:

  1. THE LAYOUT WE ARE LEAVING IS CACHED under its own column count. This is the load-bearing step: without it, 12 → 1 → 12 would have to re-derive twelve columns from a one-column stack and every tile would come back full width.
  2. GROWING RESTORES FROM THE CACHE, per item. Items the cache knows come back at exactly the cells they had the last time the board was this wide; items it does not know (added while narrow) fall through to the scale rules below. Shrinking never restores — a narrower layout is always derived from where the board is NOW, which is what makes a gradual squeeze look natural.
  3. THE REMAINDER IS SCALED by layout (see {@link GridColumnLayout}), columns === 1 forcing a single stack, and everything is re-placed in reading order so the result cannot overlap.

Returns true when the count actually changed.

  • saveLayout(): { columns: number; items: GridPackItem[] } — The layout to PERSIST — gridstack's save(), which serialises from the LARGEST cached column count rather than the live one. Saving while the board is narrow (a phone) therefore saves the DESKTOP layout: the cells a user authored at full width are the document, and the narrow arrangement is a projection of it.

h is never cached because it does not depend on the column count, so it always comes from the live item; items the widest cache does not know (added while narrow) contribute their live cells, which are legal at any count at least as wide.

  • cachedColumns(): number[] — The column counts the cache currently holds a layout for (ascending).
  • getLayouts(): GridLayoutCache — The cache as plain data — hand it to a rebuilt engine via {@link setLayouts}.
  • setLayouts(cache: GridLayoutCache): void — Adopt a cache exported by {@link getLayouts}. The kit calls this after rebuilding its engine from the model (sync()), which would otherwise throw the wider layouts away on every undo, add or refresh.

Interfaces

CachedCell

One item's cached geometry at a column count. h is column-independent.

ts
interface CachedCell

Properties

NameTypeDefaultDescription
xnumber
ynumber
wnumber

GridPackItem

One tile, in integer grid cells.

ts
interface GridPackItem

Properties

NameTypeDefaultDescription
idstring
xnumber
ynumber
wnumber
hnumber
locked?booleanPinned: never pushed, never packed, refuses the mover outright (E4b).
solid?booleanSOLID (tile first, step 3): a container. Never pushed by a PASSING tile and never packed — a widget carried over it slides aside, so the board under the hand holds still and the container's zones stay reachable — but moved by INTENT: a mover that asks pushSolid (a moved section, a dock, a refused adoption), placeBeside, or a direct move of its own.
minW?numberPer-item SIZE LIMITS in cells (gridstack's minW/maxW/minH/maxH). A resize clamps to them, and a column change scales a width only within them. They never move a tile: a push or a swap is not a resize.
maxW?number
minH?number
maxH?number
autoPosition?booleanAsk add() to IGNORE x/y and scan row-major for the first free hole — gridstack's autoPosition (addWidget without coordinates). Distinct from gravity, which climbs one column: a hole at (3,0) under an occupied column is only reachable by the scan. Seeding/load honours explicit cells; palette-style adds pass this flag.

GridPackOptions

ts
interface GridPackOptions

Properties

NameTypeDefaultDescription
columns?numberColumn count of the board. Default 12.
float?booleanFloat mode (gridstack float: true): tiles stay where placed and gaps are legal; gravity does not pack. Default false (gravity).
maxRows?numberRow bound for a board whose DESIGN is a fixed strip (the nested KPI section: one row, always). Any op whose settled result would exceed it — a height resize, a width resize whose push spills a sibling down, a move displacing someone out of bounds — ROLLS BACK wholesale and reports changed: false; add() returns null when nothing can fit.
capacity?numberRow CAPACITY of a board whose frame can hold only so many rows — the dashboard kit's bounded fit mode (a fit board never changes size; past its row floor it refuses rather than overflows).

GridPackResult

Result of a move/resize attempt.

ts
interface GridPackResult

Properties

NameTypeDefaultDescription
changedbooleanWhether the board accepted (and applied) the change.
refusedBy?GridPackRefusalPresent on every refusal, absent on an accepted change.

MoveCheckOptions

Options for {@link GridPackEngine.moveCheck}.

ts
interface MoveCheckOptions

Properties

NameTypeDefaultDescription
gate?booleanThe >50% anti-jitter coverage gate AND the swap heuristics it feeds. gate: false is the FIRST-PLACEMENT mode (a palette drag-in entering the board, or a dragged-out tile re-entering — gridstack's dragInNode behaviour): the cell is taken unconditionally — locked tiles still refuse (E4b), but any unlocked occupant is pushed down regardless of coverage, and no swap shape fires (an entering tile …
pushSolid?booleanPush SOLID tiles in the way as if they were plain — the mover means it (a moved section, a dock, a refused adoption). Default false.

PlaceBesideResult

Also has every member of GridPackResult, listed on its own entry.

Result of {@link GridPackEngine.placeBeside}.

ts
interface PlaceBesideResult extends GridPackResult

Properties

NameTypeDefaultDescription
how?'placed' | 'shifted' | 'pushed'placed: the cell beside the neighbour was free (or only unlocked tiles were in it, pushed down); shifted: no room on that side of the board, the neighbour moved over by the mover's span and the mover took the edge; pushed: no room even shifted — the mover took the cell and the neighbour went down under it, the way any tile gives way.

ResizeCheckOptions

Options for {@link GridPackEngine.resizeCheck}.

ts
interface ResizeCheckOptions

Properties

NameTypeDefaultDescription
pushSolid?booleanGrow INTO solid tiles, pushing them, instead of clamping at them — a dock taking its band means it. Default false.

Types

BesideSide

Also has every member of String, listed on its own entry.

The side of a neighbour a tile is placed against.

ts
type BesideSide = 'left' | 'right' | 'top' | 'bottom';

GridColumnLayout

Also has every member of String, listed on its own entry.

How a COLUMN-COUNT change re-lays the board out — gridstack's ColumnOptions, recorded from its documented semantics:

'moveScale' (default) — scale both x and w by newColumns/oldColumns, so the board keeps its proportions at any width; 'move' — scale x only; widths survive verbatim (clamped to fit); 'scale' — scale w only; x survives verbatim (clamped to fit); 'none' — keep x and w exactly; clamp only what no longer fits.

Whatever the mode, columns === 1 forces a SINGLE STACK (every tile x=0, w=1) — the phone layout — and every mode re-places the result in reading order afterwards, so the outcome can never overlap.

ts
type GridColumnLayout = 'moveScale' | 'move' | 'scale' | 'none';

GridLayoutCache

The PER-COLUMN LAYOUT CACHE, as plain data: column count → item id → cell. Exported/imported so a host that rebuilds its engine (the kit's sync()) carries the wider layouts across the rebuild instead of losing them.

ts
type GridLayoutCache = Record<number, Record<string, CachedCell>>;

GridPackRefusal

Also has every member of String, listed on its own entry.

Why a change was refused (tile first, step 1). A binder used to read changed:false as "refused" when it also means "already there", and asked the tile where it was instead; now every refusal names itself.

ts
type GridPackRefusal = 'locked' | 'solid' | 'bound' | 'gate' | 'noop' | 'missing';

Was this page helpful?

Layout — Grid Pack — Grafloria