@cratis/event-models

中文文档
0.342.1发布于 2026/11/11MITTypeScriptES Module近 30 天下载 7.3万

The Cratis event model board as a configurable React component.

原始使用文档
文档目录14 节
  1. Install
  2. Render a board
  3. Show proposal changes and focus an element
  4. Show host findings on slices and events
  5. Preview a suggested slice before adding it
  6. Select scopes, emphasize dependencies and draw slice links
  7. Reusable given contexts
  8. Export a whole-model PNG
  9. Edit artifact properties
  10. Reusable editor components
  11. Open the screen editor from the board
  12. Logo, view options and your own tools
  13. Styling
  14. Translators: projection and CDC

@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-dom

Everything 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>
);
  • readEventModelDocument reads a document shaped like the published JSON Schema (@cratis/event-models/schema) into an EventModel.
  • The board fills its parent, so give the parent a height.
  • readOnly draws the board without any editing gestures. Leave it off and pass editor to 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.

Show host findings on slices and events

Add optional annotations to each slice in your document:

"annotations": [
  { "severity": "information", "message": "Registration is the entry point." },
  { "severity": "error", "code": "HOST001", "message": "Registration requires a caller.", "eventId": "48800000-0000-4000-8000-000000000006" }
]

Each finding requires severity (information, warning or error) and a string message; code and the targets below are optional. The reader rejects unknown severities with InvalidEventModelDocument and the offending path. The header badge uses the worst severity and lists every finding in its tooltip and accessible label. Localize its words through EventModelMessages.annotations.

An eventId targets the card's own id within that slice, including a reference card's id rather than its sourceEventId. Matching cards get a dashed outline and a message badge; unmatched ids are ignored by the events row. Other optional ids target other cards of the same slice: commandId the command card, readModelId one of its read model cards (the first or an additional one), and specificationId one specification. A finding with a specificationId and no specificationStep marks that specification's header; with specificationStep (given, givenReadModel, when, append or then) and a stepIndex (its position among the steps of that kind, starting at 0) it is aimed at one step, and the host places it. The reader rejects an unknown specificationStep. Cards that carry a finding get the same dashed outline and badge as an event card. Ids that match nothing are ignored.

Findings work on read-only and collapsed headers and never change layout estimates. Omit annotations to keep the existing rendering. The built-in State View ordering warning shares the marker but its check is unchanged. See the annotations fixture and the Slices Area Annotations story.

Preview a suggested slice before adding it

A host that suggests slices can show one on the board before anything is stored: put it in the feature's slices with "preview": true, the way it would look once added.

<BoardProvider actions={{ slicePreview: {
    label: 'Suggestion',
    applyLabel: name => `Add ${name}`,
    cancelLabel: name => `Stop previewing ${name}`,
    onApply: async sliceId => { await addForReal(sliceId); },
    onCancel: sliceId => stopPreviewing(sliceId),
} }}>

The board draws a preview itself, ghosted and read-only, even when the host supplies editor slots: no slot is asked about it, nothing in it can be edited, dragged or reordered, and it has no specifications row. Its header shows the label, the name, a checkmark that calls onApply and a cross that calls onCancel. Both buttons wait while onApply runs, or while isApplying(sliceId) says the host is applying it from elsewhere; errorFor(sliceId) shows why applying did not go through. Without slicePreview the preview is only shown. A FeatureHeaderSuggestion with onPreview gets an eye button (previewLabel, isPreviewing) for the host to toggle the preview with. See the previews fixture and the Collection Grid Previews story.

A host that shows how slices depend on each other selects a whole slice, feature or module and tells the board how to emphasize the rest. All of it works on a read-only board, and a host that sets none of it renders exactly as before.

<EventModelBoard document={document} readOnly
    sliceLinks={[{ from: registerId, to: listId, emphasis: 'crossing', label: 'reads' }]}
    editor={{ selection: {
        selectedScopeId: selected,
        onScopeSelected: (id, kind) => setSelected(id),
        emphasis: id => emphasisOf(id),
    } }} />
  • onScopeSelected(id, kind: 'slice' | 'feature' | 'module') is raised by a click on a slice, feature or module header, or by Enter or Space when the header has keyboard focus. A header is focusable and selectable only when the callback is supplied. Clicks on the header's own buttons, menus and inputs, and presses that pan or drag the board, do not select; card selection (onArtifactSelected) is unchanged.
  • selectedScopeId marks that header as current and outlines it.
  • emphasis(id) returns 'selected', 'dependency', 'dependant', 'dimmed' or undefined for a slice, feature or module id. Each is drawn with its own outline style (solid, dashed, dotted, faded) and a marker shape on the header, and is described to assistive technology in words, so it never rests on color alone. Colors come from the --board-emphasis-* tokens in the theme; the words from EventModelMessages.scopes.
  • sliceLinks are drawn as arcs above the board between the tops of two slices, in a layer over the collections like the links between collections. They follow pan, zoom, moving and collapsing: a slice inside a collapsed feature or module attaches to that feature or module. crossing links are dashed, thicker and hollow-headed as well as differently colored (--board-slice-link-*). HostedEventModelBoard takes the same prop and uses BoardCollections.path (or contains) to find a hidden slice's ancestor.
  • handle.revealSlice(id, options?) brings a slice into view, the way scrollTo does.

Emphasis is a separate prop rather than slice annotations: annotations are findings stored in the document, while emphasis is view state that changes with every selection.

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 propPurpose
name, kindAccessible/display name and ArtifactCardKind (command, readModel, event, projection, capture)
editorComponents SchemaEditorProps except layout and aria-label; tree layout and the artifact's accessible name are package-owned
targetLabelLocalized panel heading; defaults to Target
headerLeading, headerActionsHost connector and header-action slots
headerStyle, onHeaderClick, rootDropTargetMapping highlight, root selection and root drop marker
beforeProperties, afterCardHost 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 propPurpose
name, kind, schema?Artifact and initial/current schema; omitted schema starts as an empty object
sourceRectViewport origin offered to custom dialog chrome
onSchemaChange, onCloseComplete-schema edits and dismissal; dismissal never emits another edit
labels, targetLabelComponents property-control labels and panel heading
frameOptional ComponentType<PropertySchemaDialogFrameProps>; receives name, kind, sourceRect, onClose, children
renderContentOptional 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 quill

PrimeReact 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.
  • ScreenEditorOverlay takes an environment built with createScreenEditorEnvironment. 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 from onClose; 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, or false to 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; pass fileUpload: { 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: ScreenEditorOverlay floats it over a page and ScreenEditorWorkspace sets 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 your fileUpload.handler in 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: false installs neither chart.js nor quill: 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>
  • showLogo draws the Cratis logo in the upper left.
  • showViewOptions adds a View menu to the toolbar in the upper right, for detail level, properties and how connections are drawn.
  • toolbar puts 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 SliceInteraction picks from priorEventOptions (public events, its own events and duplicates are left out). An input translation keeps picking from externalEventOptions, the events of other modules.
  • While the right cell is empty, the add control is a menu: the applicable actions.interactionRow entries (Add command) and, offered by editing.onProjectionAdded (output) or editing.onCaptureAdded (input), a projection or CDC.
  • actions.projectionCard and actions.captureCard are the card surfaces (activation and edge actions, such as opening a mapping editor or removing the card); editing.onProjectionRenamed and editing.onCaptureRenamed rename in place.

版本与发布记录

@cratis/event-models 共发布 553 个版本,首次发布于 2026/10/01;过去 12 个月发布了 553 个版本。

版本发布日期距今
0.342.1latest2026/11/11今天
0.342.02026/11/11今天
0.341.12026/11/11今天
0.341.02026/11/11今天
0.340.02026/11/11今天
0.339.32026/11/10今天

关于 @cratis/event-models 的常见问题

@cratis/event-models 是什么?

The Cratis event model board as a configurable React component.

如何安装 @cratis/event-models?

在项目目录运行 npm install @cratis/event-models,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(^19.3.0)、pixi.js(^8.22.0)、react-dom(^19.3.0)、primeicons(^8.0.2)、@cratis/scene(^0.342.1)、@cratis/components(^4.35.0)、@cratis/fundamentals(^7.22.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@cratis/event-models 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@cratis/event-models 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@cratis/event-models 可以商用吗?

@cratis/event-models 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@cratis/event-models 的下载量有多少?

根据 npm 官方下载统计,@cratis/event-models 在 2026/10/11 – 2026/11/09 这 30 天内被下载 72,620 次,日均约 2,421 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@cratis/event-models 还在维护吗?

最新版本 0.342.1 发布于 2026/11/11(今天);至今共发布 553 个版本,其中过去 12 个月发布了 553 个;首次发布于 2026/10/01。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。