# Layout — Grid Pack

Import these from `@grafloria/engine`.

## Classes

### `GridPackEngine`

```ts
class GridPackEngine
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `float` | `boolean` |  |  |
| `maxRows?` | `number` |  | Row bound (see GridPackOptions.maxRows). Undefined = unbounded. Changed through {@link setBound}. |
| `capacity?` | `number` |  | Row 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**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `x` | `number` |  |  |
| `y` | `number` |  |  |
| `w` | `number` |  |  |

### `GridPackItem`

One tile, in integer grid cells.

```ts
interface GridPackItem
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` |  |  |
| `x` | `number` |  |  |
| `y` | `number` |  |  |
| `w` | `number` |  |  |
| `h` | `number` |  |  |
| `locked?` | `boolean` |  | Pinned: never pushed, never packed, refuses the mover outright (E4b). |
| `solid?` | `boolean` |  | SOLID (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?` | `number` |  | Per-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?` | `boolean` |  | Ask 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**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `columns?` | `number` |  | Column count of the board. Default 12. |
| `float?` | `boolean` |  | Float mode (gridstack `float: true`): tiles stay where placed and gaps are legal; gravity does not pack. Default false (gravity). |
| `maxRows?` | `number` |  | Row 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?` | `number` |  | Row 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**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `changed` | `boolean` |  | Whether the board accepted (and applied) the change. |
| `refusedBy?` | `GridPackRefusal` |  | Present on every refusal, absent on an accepted change. |

### `MoveCheckOptions`

Options for {@link GridPackEngine.moveCheck}.

```ts
interface MoveCheckOptions
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `gate?` | `boolean` |  | The >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?` | `boolean` |  | Push 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**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `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**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `pushSolid?` | `boolean` |  | Grow 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';
```
