Messaging
The canvas shapes can announce what happens on them over the @cratis/arc.react messenger — region membership changes, committed note edits, sent chat messages. This is strictly opt-in and additive: every shape’s existing callback contract is unchanged, and a board that ignores all of this behaves exactly as before.
Silently inert without Arc
Section titled “Silently inert without Arc”Nothing here requires Arc to be wired up. Without an ArcContext above the Canvas — a Storybook story, a test, a host that simply does not use Arc — every shape renders and behaves exactly as it always has: nothing throws, nothing is required, and no message reaches anyone. Publishing resolves the nearest messenger the same way useMessenger() does (a MessengerScope between the Arc root and the canvas is honored), and when a host mounts an ArcContext.Provider whose configuration carries no messenger, the shapes stay silent rather than failing.
To receive the messages, subscribe on the same messenger anywhere under the same context:
import { useOnMessage } from '@cratis/arc.react/messaging';import { ItemAddedToRegion, ItemRemovedFromRegion } from '@cratis/components/Canvas';
function Board() { useOnMessage(ItemAddedToRegion, message => { // message.regionId, message.itemId — persist membership, issue a command, whatever the board means by it }); useOnMessage(ItemRemovedFromRegion, message => { /* ... */ }); // ... render the Canvas}Identity: the id props
Section titled “Identity: the id props”Messages are keyed by ids the host already owns, opted in per shape:
CanvasItemaccepts an optionalid. When set, the item registers in the Canvas item registry under that id instead of an internal generated one — which is what makes it addressable in region-membership reports. Items without anidbehave exactly as before and are anonymous to containment detection.- The
CanvasItemwrapping aRegionmust carryid={region.id}. That convention is how the region recognizes its own registry entry and excludes itself from its own containment reports. Chataccepts an optionalid. Only a chat with an id publishesChatMessageAdded.Noteneeds no new prop —NoteTextChangedcarries thenote.idit already has.
Message catalog
Section titled “Message catalog”| Message | Fields | Published when |
|---|---|---|
ItemAddedToRegion | regionId, itemId | A sibling CanvasItem carrying an id newly has its center point within a Region’s bounds — whether the item moved into the region, or the region was moved/resized over it. Also published on mount for every item already contained, so a subscriber never misses the initial state. |
ItemRemovedFromRegion | regionId, itemId | Such an item’s center point leaves the region’s bounds — including by the item unmounting. |
NoteTextChanged | noteId, text | A Note edit is committed (the editor loses focus) — the same moment onTextChange fires, never per keystroke. Carries the full committed text. |
ChatMessageAdded | chatId, text | A Chat given an id sends a message from its composer — the same moment onSend fires. |
All of them are plain classes with constructor(readonly ...) fields, importable from @cratis/components/Canvas.
Containment is by center point, inclusive of the region’s edges, and the pure function behind it — itemsWithinRegion(regionBounds, items, excludeId?) — is exported for hosts that want the same math outside the messenger flow (hit-testing a drop, say). Overlapping regions may each claim the same item; resolving exclusivity is deliberately the host’s job, as is everything else membership implies — the region never moves members, persists nothing, and knows nothing about item types (see Regions).
Defining your own messages for your own shapes
Section titled “Defining your own messages for your own shapes”The catalog above is the pattern, not a closed set. A host with its own shapes on the board publishes its own analogous messages the same way: a plain class whose constructor fields carry the ids and payload, published on the messenger at the moment the interaction commits.
// The message: a plain class, ids and payload as readonly constructor fields.export class CardFlipped { constructor( readonly cardId: string, readonly faceUp: boolean, ) {}}
// Inside the shape, at the commit point of the interaction:import { useMessenger } from '@cratis/arc.react/messaging';
const messenger = useMessenger();const handleFlip = () => { onFlip(card.id); // the callback contract stays primary messenger.publish(new CardFlipped(card.id, !card.faceUp));};Keep the shape’s callback as the primary contract and treat the message as an additive announcement, publish once per committed interaction rather than per intermediate frame, and key by the id the host already owns — then your shapes compose with subscribers exactly like the built-in ones do.