Have more questions? Join our

This documents framework v1, which is deprecated. Games already on it keep running and their authors can keep editing them, but new games are created on the current framework — start with React frontends.

Spaces

A space is an instance of a SpaceDef subclass. Spaces are auto-laid-out flexbox containers for pieces. The subclass declares kind, layout type, position (x/y), default dimensions, and optional playerId, privacy flags, scroll behavior, nested pieces, and a render() override.

import { SpaceDef } from "boardweaver";

class GridCell extends SpaceDef {
  readonly kind = "grid-cell";
  readonly type = "Stack" as const;
  readonly width = 130;
  readonly height = 130;
  readonly x: number;
  readonly y: number;

  constructor({ x, y }: { x: number; y: number }) {
    super();
    this.x = x;
    this.y = y;
  }
}

new GridCell({ x: 0, y: 0 });

Required fields

Field Type Notes
kind string Unique class id across all SpaceDefs. Used by state.spaces("grid-cell", ...).
type "Stack" | "Horizontal" | "Vertical" Layout for piece children: stacked (overlapping), row, column.
x number px from the board origin.
y number px from the board origin.
width number px. Read via space.width; no per-instance override.
height number px.

Optional fields

Field Type Default Notes
playerId number unset Marks this space as owned by a player. Per-player config spaces set this automatically.
isPrivate boolean false When true, privateState on pieces inside this space is scrubbed to null for viewers other than playerId's owner.
isHidden boolean false When true, the space is omitted from the render entirely. Mutable at runtime via space.isHidden.
scrollable boolean false Adds scroll arrows when the contained pieces overflow. Mutually exclusive with render.
pieces Record<string, PieceDef> unset Initial piece children. Same key→id mapping as top-level pieces. Each piece's spaceId is set automatically.
static Static (generic) {} Constant per-class data. Never on the wire. Read via space.def.static.
publicState PublicState merged from constructor + publicDefaults()
privateState PrivateState merged from constructor + privateDefaults() Scrubbed for non-owner viewers when isPrivate.
render SpaceRenderFn unset See below.
onMouseEnter (event, space, state, clientState) => void unset Viewer-local.
onMouseLeave same shape unset

render() — custom space chrome

import { SpaceDef, type SpaceRenderFn } from "boardweaver";

class Deck extends SpaceDef<{}, {}, {}, { render: true }> {
  readonly kind = "deck";
  readonly type = "Stack" as const;
  readonly width = 120;
  readonly height = 170;
  readonly x = 0;
  readonly y = 0;

  readonly render: SpaceRenderFn = (space, state, ctx) => ({
    type: "Box",
    style: {
      width: space.width,
      height: space.height,
      borderRadius: 8,
      backgroundColor: "$colors.surface.canvas",
      padding: 4,
    },
    children: [
      { type: "Pieces" },                            // insertion point for piece children
      `${space.pieces().length} cards`,
    ],
  });
}

Signature: (space, gameState, ctx, clientState) => SpaceRenderResult.

Result shapes:

  • SpaceBwssNode — visuals only; piece children appear at the embedded Pieces node.
  • { node, width?, height? } — visuals plus per-call dimension override.
  • null — fall back to the default container (same as if render was unset).

ctx is { mode: "board"; isSelectable: boolean }.

Pieces node placement

The rendered tree MUST contain exactly one { type: "Pieces" } node — this is where the framework injects the laid-out piece children.

  • Zero Pieces nodes → renders no pieces (console warning).
  • Multiple Pieces nodes → rejected (console warning); falls back to the default container.

Generic parameters

abstract class SpaceDef<
  PublicState  extends Record<string, unknown>       = {},
  PrivateState extends Record<string, unknown>       = {},
  Static       extends Record<string, unknown>       = {},
  Flags        extends { render?: boolean }          = {},
  ClientState  extends Record<string, unknown>       = {},
  GlobalClientState extends Record<string, unknown>  = {},
>

When a subclass opts into Flags = { render: true }, scrollable narrows to false | undefined at compile time — setting scrollable: true is a TS2322. The runtime check in toConfig is defense in depth.

Gotchas

  • render and scrollable: true are mutually exclusive — throws at game start. For scrolling inside a custom render, set style.overflow: "auto" on a container or the Pieces node.
  • A space's width / height come from the registered SpaceDef class — there is no per-instance override at runtime.
  • playerId matters for privacy: pieces inside a isPrivate space have their privateState scrubbed to null for every viewer except playerId's owner.
  • A kind you only state.addSpace at runtime must still be registered in gameConfig().spaceDefs or it throws when added.
  • isHidden: true removes the space from the render but keeps it in state — its pieces are still queryable via state.pieces() / state.spaces().