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().