schedgrid/core

createGrid, the engine. No platform dependencies.

import { createGrid } from 'schedgrid/core';

createGrid holds the state, rebuilds the expensive pieces only when their own inputs change and the cheap frame whenever anything does. Nothing in this module touches a platform API. It compiles without the DOM lib.

createGrid(options)

Prop

Type

rows and columns each describe one axis:

Prop

Type

A size is a SizeSpec: either a constant number of pixels or a function of the index, which is how a header row gets a different height from the body rows:

type SizeSpec = number | ((index: number) => number);

rows: { count: 201, size: (i) => (i === 0 ? 32 : 40), pinned: 1 }

lanes is 'rows', 'columns' or false. See Concepts.

Items

Prop

Type

The grid

The object createGrid returns. Every setter replaces one slice of state and notifies subscribers once; wrap several in batch to notify once for all of them.

Prop

Type

rows and columns expose read-only queries on each axis, all in content space:

Prop

Type

The frame

Everything a renderer needs to draw one frame. Plain numbers only.

Prop

Type

Prop

Type

Prop

Type

frame.cells() builds the cross product of the frame's rows and columns on first call and caches it. Use it when every cell needs a DOM node or a canvas fill; skip it when only lines and items are drawn.

Prop

Type

frame.range is the scrollable index range the frame covers, pinned entries excluded — the rows and columns a data source would need to fetch:

Prop

Type

total, viewport and scroll are the two shapes every measurement in the library uses:

Prop

Type

Prop

Type

State

getState() returns the resolved options — the same shape you passed to createGrid, with the defaults filled in:

Prop

Type

Utilities

The module also exports the pieces the engine is built from, for anyone who wants to lay out items without a grid.

assignLanes(items, mode)

function assignLanes(items: readonly Item[], mode: LaneMode): Lane[];

One Lane per item, in the order the items were given. Greedy interval colouring generalised to rectangles: items are swept along one axis in start order and each takes the lowest lane not used by an overlapping item already placed. Overlapping items are unioned into a cluster and every member reports the cluster's lane count, so a renderer can give each item 1 / lanes of the shared extent.

It runs over every item, not just the visible ones — an off-screen item can still push a visible one into a higher lane, and lanes must not shift as the user scrolls. The grid memoises the result on items identity, so passing the same array back costs nothing.

Prop

Type

mode is the same LaneMode the grid takes, and picks which dimension is divided when items overlap. See Concepts.

type LaneMode = 'rows' | 'columns' | false;

itemsIntersect(a, b) and rangesIntersect(a, b)

function rangesIntersect(
  a: readonly [number, number],
  b: readonly [number, number],
): boolean;
function itemsIntersect(a: Item, b: Item): boolean;

Overlap of half-open ranges: touching ranges do not intersect, so [0, 3] and [3, 5] are apart. itemsIntersect is rangesIntersect on both axes.

createStore(initial)

function createStore<T extends object>(initial: T): Store<T>;

The subscribable store behind getState, subscribe and batch: the smallest one useSyncExternalStore is happy with. Snapshots are immutable — a new object per change — and set skips the notification entirely when every patched key is Object.is-equal to the current value, which is why a scroll handler reporting the same offset twice costs nothing downstream.

Prop

Type