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.
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 });
| 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. |
| 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 chromeimport { 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 placementThe rendered tree MUST contain exactly one { type: "Pieces" } node — this is where the framework injects the laid-out piece children.
Pieces nodes → renders no pieces (console warning).Pieces nodes → rejected (console warning); falls back to the default container.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.
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.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.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().