@cratis/event-models
v0.189.0
Published
The Cratis event model board as a configurable React component.
Readme
@cratis/event-models
The Cratis event model board as a React component. It is the same board Cratis Studio draws, both the editor and the read-only viewer.
The package is released together with Cratis Studio and carries the Studio version. While that is 0.x its API can still change between minor versions, so pin a minor version, for example ~0.105.0, and install the same version of @cratis/scene.
Install
npm install @cratis/event-models @cratis/scene @cratis/components @cratis/fundamentals primeicons pixi.js react react-domEverything except the board itself is a peer dependency, so your application decides the versions.
Render a board
import { EventModelBoard, readEventModelDocument } from '@cratis/event-models';
import '@cratis/components/styles';
import '@cratis/event-models/theme';
import '@cratis/event-models/styles';
import '@cratis/scene/styles';
import 'primeicons/primeicons.css';
const model = readEventModelDocument(json);
export const Board = () => (
<div style={{ height: '100vh' }}>
<EventModelBoard document={model} readOnly />
</div>
);readEventModelDocumentreads a document shaped like the published JSON Schema (@cratis/event-models/schema) into anEventModel.- The board fills its parent, so give the parent a height.
readOnlydraws the board without any editing gestures. Leave it off and passeditorto receive the board's edits as callbacks. The package never persists anything by itself.
Show proposal changes and focus an element
Pass changes to mark modules, features, slices or artifact cards, including on a read-only board:
const handle = useRef<EventModelBoardHandle>(null);
const changes: BoardChanges = new Map([[sliceId.toString(), 'changed']]);
<EventModelBoard document={model} readOnly handle={handle}
changes={changes} initialFocus={{ id: sliceId }} />
await handle.current?.scrollTo(anotherElementId, { animate: false });BoardChange is 'added' | 'changed' | 'removed'; BoardChanges is a read-only map or (id: string) => BoardChange | undefined. Ids are case-insensitive, and resolver inputs are lower-case Guid strings. Replace a map when its entries change. BoardMarkers carries the optional changes channel through BoardProvider. The badges use text and SVG icons, not an icon font; EventModelMessages.changes optionally localizes their words. Exports retain markers. To show a removed ghost, keep that element in the document and mark it 'removed'; the board does not merge earlier documents.
initialFocus?: BoardFocus ({ id: Guid | string }) opens once without animation when the camera is ready. scrollTo(id, options?: BoardScrollOptions): Promise<boolean> accepts collection, module, feature, slice and card ids; animation defaults to true. It mounts offscreen collections first and falls back to the nearest visible ancestor for collapsed elements. Unknown ids return false. Call scrollTo after switching documents if Current and Proposed keep the same document id. HostedEventModelBoard offers the same props; its BoardCollections.contains callback locates nested ids in host-shaped collections. See the board guide for details.
Reusable given contexts
A specification's optional givenContexts list preserves references in use order. Each entry carries givenContextId, and may carry the current name, kind (GivenContextKind.events or readModel) and missing status. Leave metadata unresolved while its source is pending; set missing: true only after resolving a deleted reference. The board shows broken references rather than hiding them.
Hosts composing GivenSection can supply the domain's available contexts through its contexts prop and localize its contextLabels. Supply onGivenContextUsed and onGivenContextRemoved through BoardEditing; each returns a promise resolving to whether persistence succeeded. The controls lock while awaiting the result. Events contexts are offered to State Change slices; State View slices accept one read-model context or inline read-model state, not both. Hosts remain responsible for validating those rules and expanding references when executing or exporting specifications.
Export a whole-model PNG
Pass a React ref as handle to EventModelBoard and call handle.current.exportImage() to obtain a PNG Blob. The public exportBoardImage function also captures a board from collection renderers without a visible board mount.
Capture renders every collection, including those outside the viewport, in a separate read-only instance. Its positioning shell stays offscreen, but only the origin-relative content is rasterized. Content-visibility culling, containment, animations and transitions are disabled only inside that capture instance; the interactive board retains them. Import the published stylesheet as above. No host CSS override, animation delay or Studio provider is needed.
The standalone regression consumer in scripts/event-model-image imports the built package and its extracted stylesheet, with a dark host and long-running motion styles. Its browser probe requires colored cards and dark text pixels in both viewport and off-viewport regions, not merely PNG dimensions. See its README for running it.
Edit artifact properties
Supply any of these optional BoardEditing callbacks through editor.editing:
<EventModelBoard document={model} editor={{ editing: {
onCommandPropertiesChanged: (sliceId, commandId, schema) => keepCommandSchema(sliceId, commandId, schema),
onReadModelPropertiesChanged: (sliceId, readModelId, schema) => keepReadModelSchema(sliceId, readModelId, schema),
onEventPropertiesChanged: (sliceId, eventId, schema) => keepEventSchema(sliceId, eventId, schema),
} }} />Each callback receives the complete JsonSchema, from @cratis/components/SchemaEditor, after every edit. Despite the callback names, this is not a Property[]. The host owns persistence, errors and batching. Update your local board document in the callback and persist separately, so delayed server snapshots cannot overwrite an edit. The package does not cache an unacknowledged schema across card mounts or mask authoritative host updates. Studio retains its existing optimistic state and command save-on-close policy in its host adapters.
Clicking a card opens its properties-only editor only when its callback exists and its slice and board are editable. Existing BoardActions card onActivate callbacks take precedence, allowing a host to open its advanced editor instead. No rules, concurrency, projection or constraint editors are included by default.
The shared tree supports adding, renaming, retyping and removing properties, nested objects and arrays, and concepts. id remains protected. Command schemas offer an identifier. Event schemas offer required flags, and a property added to an event is required until the user makes it optional, because Cratis events have no optional properties. Commands and read models offer no required flags. The PropertiesOnly board story demonstrates all three callbacks without Studio.
Reusable editor components
PropertySchemaCard and PropertySchemaDialog are exported from the package root. Import the package stylesheet as for the board. A host can reuse these within its existing editor without copying property UI.
| PropertySchemaCard prop | Purpose |
| --- | --- |
| name, kind | Accessible/display name and ArtifactCardKind (command, readModel, event, projection, capture) |
| editor | Components SchemaEditorProps except layout and aria-label; tree layout and the artifact's accessible name are package-owned |
| targetLabel | Localized panel heading; defaults to Target |
| headerLeading, headerActions | Host connector and header-action slots |
| headerStyle, onHeaderClick, rootDropTarget | Mapping highlight, root selection and root drop marker |
| beforeProperties, afterCard | Host validation panel and dismissing backdrop slots |
The editor slots can attach rule rows, mapping connectors, rename/delete cleanup and validation while the shared component retains its property controls. Controlled properties mode and all its callbacks remain available for mapping editors; schema/onChange provide schema-managed mode. Host policies may override required/key availability.
| PropertySchemaDialog prop | Purpose |
| --- | --- |
| name, kind, schema? | Artifact and initial/current schema; omitted schema starts as an empty object |
| sourceRect | Viewport origin offered to custom dialog chrome |
| onSchemaChange, onClose | Complete-schema edits and dismissal; dismissal never emits another edit |
| labels, targetLabel | Components property-control labels and panel heading |
| frame | Optional ComponentType<PropertySchemaDialogFrameProps>; receives name, kind, sourceRect, onClose, children |
| renderContent | Optional host-owned advanced content; receives the schema-change callback |
The default frame is a dismissable Components Dialog, 440px wide, with content-focused initial focus and no confirm footer. Studio supplies its existing flying dialog through frame, and attaches advanced content through the card's slots. EventModelMessages.propertyEditor optionally provides target and labels to built-in board editors. propertySchemaChangeHandler(editing, kind, sliceId?, artifactId?) binds the matching callback to both identities, or returns undefined when editing is unavailable.
Open the screen editor from the board
The board reports when someone selects a slice's screen preview. @cratis/event-models/screen-editor is the editor that opens over it, so a host outside Cratis Studio can let people design a screen without any Studio code. It is a separate entry, and a host that only draws the board installs none of what it needs.
Install what the editor draws with. These are optional peers, required only by this entry:
npm install @cratis/scene.model @cratis/scene.engine @cratis/scene.react @cratis/scene.components @cratis/scene.primereact @cratis/scene.tailwind @cratis/scene.blueprint.default @cratis/scene.blueprint.components @primereact/core @primereact/headless @primereact/styles primereact lucide-react react-icons chart.js quillPrimeReact must be 11.2.0 or newer (@primereact/core, @primereact/headless, @primereact/styles and primereact): the editor imports component styles that 11.2.0 introduced (@primereact/styles/inputpassword), so an earlier 11 fails to resolve the entry. yarn guard:peer-floor checks every style specifier against the declared floor.
The Scene packages must be 4.7.0 or newer: that release adds the chart, multi-state checkbox, file upload, data scroller, tree table and rich text editor controls and the JSON property type the editor draws a field for. chart.js (4.5.1 or newer) draws the chart control and quill (2.0.3 or newer) is the rich text control's engine. Both are optional peers of this entry, and a host that offers those controls in its palette installs them. Quill's stylesheet is part of @cratis/event-models/screen-editor/styles, so there is nothing more to import.
Import the editor's stylesheet beside the board's, and Scene's:
import '@cratis/components/tokens';
import '@cratis/components/styles';
import '@cratis/components/theme';
import '@cratis/event-models/styles';
import '@cratis/event-models/screen-editor/styles';
import '@cratis/scene/styles';
import 'primeicons/primeicons.css';Wrap the page in the two providers the components inside a screen need:
import { CratisComponentsProvider } from '@cratis/components';
import { ScreenEditorPrimeReactProvider } from '@cratis/event-models/screen-editor';
<CratisComponentsProvider value={{ locale: 'en-US' }}>
<ScreenEditorPrimeReactProvider license={yourPrimeUiLicenseKey} theme={yourPrimeReactTheme}>
{/* the board and the editor */}
</ScreenEditorPrimeReactProvider>
</CratisComponentsProvider>ScreenEditorPrimeReactProvider is PrimeReact's own PrimeReactProvider with one addition, and it takes the same props (license, theme, locale, pass-through, defaults). The addition is the component styles: PrimeReact 11 ships its components as structure with no look, and the look comes from @primereact/styles, which a provider has to be handed as defaults. Without them a screen drawn in the editor is the right markup with none of the appearance it has in Play - a button that is plain text, a card with no surface, a field with no box. This provider hands them over (screenEditorPrimeReactDefaults), and any defaults you pass are merged into those component by component and part by part - an entry you give Button adds to its styles and never replaces them; where you set the same prop, yours wins. It is not a second context: the published bundle imports @primereact/core/config from your install, so the provider and every PrimeReact component in the page share one context. If you use PrimeReactProvider from @primereact/core/config yourself, pass defaults={screenEditorPrimeReactDefaults} to it - and merge your own entries into it with mergeComponentDefaults, since a plain object spread replaces a whole component entry, styles included. Either way, install a single @primereact/core.
- The editor needs no Studio Core, Arc, router or Studio provider. It does need a PrimeReact provider (
ScreenEditorPrimeReactProvider), because the default palette offers PrimeReact components and those throw without one. If a component does fail, the editor closes with a message and the page it was opened over carries on. - PrimeReact 11 checks a license key at runtime and shows an Invalid PrimeUI License badge without one. The key belongs to your application: pass it to the provider. The package never sees it.
ScreenEditorOverlaytakes an environment built withcreateScreenEditorEnvironment. That is where the host lends the queries a screen may bind, the state changes it may submit and the screens it can lead to, in the shapes described on the exported types. The editor hands the edited document back fromonClose; the host decides where it is kept.- Two controls need something from the host, and neither needs anything by default. The rich text control loads Quill through
richText, which defaults to the loader from@cratis/scene.primereact/quill; pass your own loader to supply the engine yourself, orfalseto leave rich text out, and the control then says it is not available. The file upload control posts to the address its author configured, to your own origin, and only when someone presses upload; passfileUpload: { handler, allowedOrigins }to own the upload yourself or to allow other origins. An author's address is never rewritten, nothing is requested while a screen is being edited, and an address on an origin you did not allow is refused before anything is sent. - The same services reach the controls wherever the editor is drawn:
ScreenEditorOverlayfloats it over a page andScreenEditorWorkspacesets it into one, as a screen template is authored, and both wrap what they draw in the same providers. The rich text control is editable in either, and an upload goes through yourfileUpload.handlerin either. - A property that holds any JSON value (a chart's data, its options) is edited as text next to its other properties. The author's text is parsed, and
null,0,false, arrays and objects are values of their own; text that does not parse is refused where it is typed and nothing is applied. Emptying the field, or choosing "Not set" on a choice, puts the property back to its default, and one undo brings it back. A value the stored document holds that is none of a choice's options (an older document) is listed under its own value, so it can be read and kept. - A host with no chart in its palette and
richText: falseinstalls neitherchart.jsnorquill: the entry type-checks without skipping library declarations and builds without either.
<EventModelBoard document={model} editor={{ editing: { onPrototypeOpened: (sliceId, actorId, rect) => open(sliceId, actorId, rect) } }} />
{opening && (
<ScreenEditorOverlay
subject={{ documentText: saved[opening.key] }}
environment={environment}
strings={defaultScreenEditorStrings}
layoutLabels={defaultLayoutLabels}
icons={defaultPaletteIcons}
paletteSources={defaultPaletteSources}
sourceRect={opening.rect}
onClose={result => keep(opening.key, result)} />
)}@cratis/components 4.25 or later is required by this entry, and is now the minimum for the package. Consumers that only draw the board need to move to it too.
Logo, view options and your own tools
The board can draw the Cratis logo and a toolbar over itself. Both are off unless you ask for them:
const [presentation, setPresentation] = useState(defaultEventModelPresentation);
<EventModelPresentationProvider presentation={presentation} onChange={setPresentation}>
<EventModelBoard
document={model}
readOnly
showLogo
showViewOptions
toolbar={<ToolbarButton icon='pi pi-refresh' title='Refresh' onClick={refresh} />} />
</EventModelPresentationProvider>showLogodraws the Cratis logo in the upper left.showViewOptionsadds a View menu to the toolbar in the upper right, for detail level, properties and how connections are drawn.toolbarputs your own tools in that toolbar, before the view options.
The view options hand the chosen presentation to the provider's onChange, so you decide where it is kept. Without onChange, the board keeps the choice itself.
Styling
@cratis/event-models/styles holds the board's own CSS. The board paints itself from design tokens: card sizes and colors, and a PrimeReact-style surface and text palette. If your application does not already define them, import @cratis/event-models/theme for the values Cratis Studio uses. Every value in it has zero specificity, so anything your application sets on :root wins.
The colors follow the page's color-scheme (light-dark()), and text uses the page's font, so set both on your page.
The board's own chrome (header labels, pills, row labels, menus, placeholders) draws from --board-ink, --board-menu-bg, --board-accent, --board-feature and the --board-placeholder-* tokens in the same stylesheet, so a light page gets a readable board. A host that does not import the theme gets the dark values, which is what the board has always drawn.
Translators: projection and CDC
A translator slice holds at most one of a command, a projection (Slice.projection, an output translation) or a CDC capture (Slice.capture, an input translation). Projection cards are pink (projectionCardColors, --card-projection-bg) and capture cards teal (captureCardColors, --card-capture-bg); each shows the names of the events it works from.
- An output translation translates the slice's earlier private events, so
SliceInteractionpicks frompriorEventOptions(public events, its own events and duplicates are left out). An input translation keeps picking fromexternalEventOptions, the events of other modules. - While the right cell is empty, the add control is a menu: the applicable
actions.interactionRowentries (Add command) and, offered byediting.onProjectionAdded(output) orediting.onCaptureAdded(input), a projection or CDC. actions.projectionCardandactions.captureCardare the card surfaces (activation and edge actions, such as opening a mapping editor or removing the card);editing.onProjectionRenamedandediting.onCaptureRenamedrename in place.
