Client libraries
Besides react and boardweaver/react, a client may import four libraries. The frame provides one shared copy of each, so they add nothing to the game's bundle, and every game uses the versions below.
| Import | Library | Use it for |
|---|---|---|
boardweaver/motion |
motion/react from motion 12.43.0 |
Animating pieces, panels and effects |
boardweaver/ui |
radix-ui 1.6.7 |
Dialogs, menus, tooltips and other accessible controls |
boardweaver/dnd |
@dnd-kit/core 6.3.1 and @dnd-kit/sortable 10.0.0 |
Dragging a piece onto a space |
zod |
zod 3.25.76 |
Parsing JSON, as boardweaver/game does |
Import only the names listed here as values. Any other value fails the commit with No matching export. Import an upstream type, such as DragEndEvent, with type: the build erases it, so it needs no listing. Importing the upstream package by its own name (motion/react, radix-ui, @dnd-kit/core) fails it with Cannot resolve import. zod is imported by its own name.
boardweaver/motion
motion, LayoutGroup, AnimatePresence, usePresence, useIsPresent, useMotionValue, useTransform, useSpring, useMotionTemplate, useMotionValueEvent, useAnimate, useInView, useAnimationFrame, MotionConfig and useReducedMotion. The props are Motion's own, so its documentation at https://motion.dev/docs/react applies.
- Animate a move from
useLastChange(). Pieces have no ids, so alayoutIdcannot follow a piece from one space to another by itself. EachMovedmovement gives the piece'sfromandtopositions: draw the piece attoand animate it in from wherefromis on screen. When the game writes its own id into a piece'spublicState, use that id as thelayoutIdinstead. - Skip the animation when
coalescedistrue. The board jumped, as What changed explains. - Respect reduced motion. When
useReducedMotion()istrue, show the end state at once.
boardweaver/ui
AccessibleIcon, Accordion, AlertDialog, AspectRatio, Avatar, Checkbox, Collapsible, ContextMenu, Dialog, Direction, DropdownMenu, Form, HoverCard, Label, Menubar, NavigationMenu, Popover, Portal, Progress, RadioGroup, ScrollArea, Select, Separator, Slider, Slot, Switch, Tabs, Toast, Toggle, ToggleGroup, Toolbar, Tooltip and VisuallyHidden.
Each one is a namespace of parts (Dialog.Root, Dialog.Trigger, Dialog.Content). The parts are unstyled: style them with the style prop or a <style> element, and target Radix's data-state attributes for open and closed styles. Radix's documentation at https://www.radix-ui.com/primitives/docs/components/<name> applies; where it imports @radix-ui/react-dialog, import { Dialog } from boardweaver/ui.
import { Dialog } from "boardweaver/ui";
export function RulesButton() {
return (
<Dialog.Root>
<Dialog.Trigger>Rules</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay style={{ position: "fixed", inset: 0, background: "#0008" }} />
<Dialog.Content style={{ position: "fixed", inset: "20%", background: "#fff", padding: 16 }}>
<Dialog.Title>How to play</Dialog.Title>
<Dialog.Description>Place a mark on an empty square.</Dialog.Description>
<Dialog.Close>Close</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
A portal renders into the frame's own document, so a dialog can never cover Boardweaver's bar or escape the frame.
boardweaver/dnd
DndContext, DragOverlay, useDraggable, useDroppable, useSensor, useSensors, PointerSensor, KeyboardSensor, TouchSensor, closestCenter, closestCorners, SortableContext, useSortable, arrayMove, verticalListSortingStrategy, horizontalListSortingStrategy, rectSortingStrategy and sortableKeyboardCoordinates. dnd-kit's documentation at https://docs.dndkit.com applies.
A drop is a move like any other: send it with act from onDragEnd. When the game takes a move as two clicks, a piece and then a space, pass both steps as one list. Make a piece draggable only while its PieceClick is offered, and keep the piece's position in the draggable's data, which onDragEnd reads back:
import type { ReactNode } from "react";
import { DndContext, useDraggable, useDroppable, type DragEndEvent } from "boardweaver/dnd";
import { useAct, useOffers, useUndo } from "boardweaver/react";
type PieceAt = { spaceId: string; index: number };
export function DraggablePiece({ spaceId, index, children }: PieceAt & { children: ReactNode }) {
const offered = useOffers().some(
({ action }) => action.type === "PieceClick" && action.spaceId === spaceId && action.index === index,
);
const data: PieceAt = { spaceId, index };
const { setNodeRef, listeners, attributes, transform } = useDraggable({
id: `${spaceId}:${index}`,
data,
disabled: !offered,
});
const style = transform ? { transform: `translate(${transform.x}px, ${transform.y}px)` } : undefined;
return (
<div ref={setNodeRef} style={style} {...listeners} {...attributes}>
{children}
</div>
);
}
export function DroppableSpace({ spaceId, children }: { spaceId: string; children: ReactNode }) {
const { setNodeRef } = useDroppable({ id: spaceId });
return <div ref={setNodeRef}>{children}</div>;
}
export function DragBoard({ children }: { children: ReactNode }) {
const act = useAct();
const { undo } = useUndo();
const onDragEnd = ({ active, over }: DragEndEvent) => {
const piece = active.data.current as PieceAt | undefined;
if (!over || !piece || over.id === piece.spaceId) return;
const outcome = act([
{ type: "PieceClick", spaceId: piece.spaceId, index: piece.index },
{ type: "SpaceClick", spaceId: String(over.id) },
]);
if (outcome.type === "failed" && outcome.applied) undo();
};
return <DndContext onDragEnd={onDragEnd}>{children}</DndContext>;
}
The space a piece may move to is offered only after its PieceClick, so every space is droppable. Ignore a drop back on the piece's own space. A drop on a space the game does not offer fails after the PieceClick joined the draft, and applied is 1: call undo() to take the PieceClick back, or the piece stays picked up. Call act once, in onDragEnd itself, as Taking a step explains.
zod
z and ZodError. This is zod 3, and its documentation at https://v3.zod.dev applies.
- Parse a map with its client type.
boardweaver/gamealready holds a schema for each client type. Write a schema withzonly for JSON no client type describes.