StatePro Studio is a visual editor for building and maintaining StatePro machine definitions.
It can be used in three main ways:
- As a local app for day-to-day authoring (
studio/app). - As an embeddable editor (
@rendis/statepro-studio-react) inside your own product. - As a framework-agnostic custom element (
@rendis/statepro-studio-web-component) for React/Vue/other frameworks.
StatePro Studio provides a graph-based UI to edit:
- Machine definition JSON (
StateProMachine). - Visual layout data (
StudioLayoutDocument). - Metadata pack registry and bindings.
On every edit, Studio serializes and validates the model, then emits a change payload so host apps can persist continuously.
cd studio
pnpm install
pnpm devInstall the package that matches your integration mode:
pnpm add @rendis/statepro-studio-react react react-dompnpm add @rendis/statepro-studio-web-component @rendis/statepro-studio-reactAdditional workspace checks:
cd studio
pnpm typecheck
pnpm test
pnpm build| Package | Purpose | Typical Consumer |
|---|---|---|
studio/app |
Local development shell for Studio | Studio contributors |
@rendis/statepro-studio-react |
React component package exposing StateProEditor and types |
React applications |
@rendis/statepro-studio-web-component |
Custom element wrapper around StateProEditor |
Framework-agnostic embedding (Vue, vanilla JS, etc.) |
StateProEditor receives external data through value or defaultValue using this shape:
interface StudioExternalValue {
definition: StateProMachine;
layout?: StudioLayoutDocument;
metadataPacks?: {
registry: MetadataPackRegistry;
bindings: MetadataPackBindingMap;
};
}Contract notes:
definitionis required.layoutis optional but recommended if you want deterministic node positions.metadataPacksis optional and can be provided externally.
StudioLayoutDocument stores visual state and metadata-pack snapshots.
Main sections:
machineRef: machine identity for layout compatibility checks.nodes: visual snapshots for universes, realities, and global notes.transitions: visual offsets and notes per serialized transition reference.packs:{ packRegistry, bindings }snapshot associated with layout.
Studio notifies every change through:
interface StudioChangePayload {
machine: StateProMachine;
layout: StudioLayoutDocument;
issues: SerializeIssue[];
canExport: boolean;
source: "user" | "external-sync";
at: string; // ISO timestamp
}Field meaning:
machine: latest serialized model.layout: latest serialized layout + pack snapshot.issues: validation output (warnings/errors).canExport:falsewhen blocking validation errors exist.source: whether change came from in-editor user actions or from controlled external synchronization.at: event timestamp.
pnpm add @rendis/statepro-studio-react react react-domimport { useState } from "react";
import {
StateProEditor,
type StudioChangePayload,
type StudioExternalValue,
} from "@rendis/statepro-studio-react";
import "@rendis/statepro-studio-react/styles.css";
const initialValue: StudioExternalValue = {
definition: {
id: "machine-id",
canonicalName: "machine",
version: "1.0.0",
universes: {},
},
};
export function EmbeddedStudio() {
const [value, setValue] = useState<StudioExternalValue>(initialValue);
const handleChange = (payload: StudioChangePayload) => {
if (payload.source === "external-sync") {
return;
}
setValue({
definition: payload.machine,
layout: payload.layout,
metadataPacks: {
registry: payload.layout.packs.packRegistry,
bindings: payload.layout.packs.bindings,
},
});
};
return <StateProEditor value={value} onChange={handleChange} />;
}import { useMemo, useState } from "react";
import {
StateProEditor,
type BehaviorRegistryItem,
type StudioChangePayload,
type StudioExternalValue,
type StudioFeatureFlags,
type StudioLocale,
type StudioUniverseTemplate,
} from "@rendis/statepro-studio-react";
import "@rendis/statepro-studio-react/styles.css";
const templates: StudioUniverseTemplate[] = [
{
id: "support-template",
label: "Support Flow",
universe: {
id: "support",
canonicalName: "support",
version: "1.0.0",
realities: {
waiting: { id: "waiting", type: "transition" },
},
},
},
];
const libraryBehaviors: BehaviorRegistryItem[] = [
{
src: "builtin:action:logArgs",
type: "action",
description: "Logs args",
simScript: "console.log(args);\\nreturn true;",
},
];
const initialValue: StudioExternalValue = {
definition: {
id: "machine-id",
canonicalName: "machine",
version: "1.0.0",
universes: {},
},
};
export function FullEmbeddedStudio() {
const [value, setValue] = useState(initialValue);
const [locale, setLocale] = useState<StudioLocale>("en");
const features = useMemo<StudioFeatureFlags>(
() => ({
json: { import: true, export: true },
library: {
behaviors: { manage: false },
metadataPacks: { create: false },
},
performance: {
mode: "auto",
staticPressureThreshold: 1200,
onEmaMs: 18,
offEmaMs: 14,
onMissRatio: 0.25,
offMissRatio: 0.1,
},
}),
[],
);
const handleChange = (payload: StudioChangePayload) => {
if (payload.source === "external-sync") {
return;
}
setValue({
definition: payload.machine,
layout: payload.layout,
metadataPacks: {
registry: payload.layout.packs.packRegistry,
bindings: payload.layout.packs.bindings,
},
});
};
return (
<StateProEditor
value={value}
onChange={handleChange}
changeDebounceMs={250}
universeTemplates={templates}
libraryBehaviors={libraryBehaviors}
features={features}
locale={locale}
onLocaleChange={setLocale}
defaultLocale="en"
persistLocale
showLocaleSwitcher
/>
);
}Studio uses Tailwind utility classes from the default color palette (slate, blue, yellow, green, red, cyan, orange, purple, sky, etc.) hardcoded in its components. Your host app must ensure Tailwind generates these classes.
@rendis/statepro-studio-react/styles.css exports custom scrollbar styles (.custom-scrollbar, .note-scrollbar). Import it or copy the scrollbar CSS into your host app.
Include Studio sources in Tailwind content scanning via tailwind.config.ts:
import type { Config } from "tailwindcss";
export default {
content: [
"./index.html",
"./src/**/*.{ts,tsx}",
"./node_modules/@rendis/statepro-studio-react/**/*.{js,ts,tsx}",
"../packages/editor-core/src/**/*.{ts,tsx}", // monorepo/workspace usage
],
theme: { extend: {} },
plugins: [],
} satisfies Config;Use the @source directive in your CSS entry point to scan Studio dist files:
@import "tailwindcss";
@source "../node_modules/@rendis/statepro-studio-react/dist/**/*.js";Important: The
@sourcepath is relative to the CSS file, not the project root. Verify the path resolves to the actualdist/directory (check for symlinks in monorepo/workspace setups).
Studio depends on Tailwind's default color palette. If your host app defines custom colors with @theme, you must use the correct syntax to avoid removing default colors:
/* Static values (fonts, animations) — preserves default color palette */
@theme {
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
--animate-fade-in: fade-in 0.2s ease-out;
}
/* Dynamic values referencing CSS variables — use @theme inline */
@theme inline {
--color-background: hsl(var(--background));
--color-primary: hsl(var(--primary));
}Using hsl(var(...)) or other dynamic values directly inside @theme (without inline) can break the generation of default Tailwind color variables (--color-slate-*, --color-blue-*, etc.), causing Studio to render with transparent/missing colors.
Global rules like * { border-color: ... } will override Studio borders. Wrap the editor in an isolation container to reset inherited styles:
.studio-wrapper * {
border-color: currentColor;
}Use the path(s) that match your setup (node_modules or workspace source path).
import {
defineStateProStudioElement,
STUDIO_CHANGE_EVENT,
STUDIO_LOCALE_EVENT,
} from "@rendis/statepro-studio-web-component";
import type { StudioChangePayload, StudioExternalValue } from "@rendis/statepro-studio-react";
import "@rendis/statepro-studio-react/styles.css";
defineStateProStudioElement();
const el = document.querySelector("statepro-studio") as HTMLElement & {
value?: StudioExternalValue;
features?: unknown;
onChange?: (payload: StudioChangePayload) => void;
};
el.value = {
definition: {
id: "machine-id",
canonicalName: "machine",
version: "1.0.0",
universes: {},
},
};
el.features = {
json: { import: true, export: true },
library: { behaviors: { manage: false }, metadataPacks: { create: false } },
performance: {
mode: "auto",
},
};
el.addEventListener(STUDIO_CHANGE_EVENT, (event) => {
const detail = (event as CustomEvent<StudioChangePayload>).detail;
console.log("studio-change", detail);
});
el.addEventListener(STUDIO_LOCALE_EVENT, (event) => {
const detail = (event as CustomEvent<{ locale: "en" | "es" }>).detail;
console.log("studio-locale-change", detail.locale);
});<statepro-studio
locale="en"
default-locale="en"
show-locale-switcher="true"
persist-locale="true"
change-debounce-ms="250"
></statepro-studio>| Attribute | Type | Notes |
| ---------------------- | ------------------- | ---------------------------------------------- | ----------------- |
| locale | "en" | "es" | Controlled locale |
| default-locale | "en" | "es" | Fallback locale |
| show-locale-switcher | boolean-like string | Show/hide language toggle in header |
| persist-locale | boolean-like string | Enable/disable localStorage locale persistence |
| change-debounce-ms | number-like string | Debounce for emitted change payloads |
| Property | Type |
|---|---|
value |
StateProEditorProps["value"] |
defaultValue |
StateProEditorProps["defaultValue"] |
universeTemplates |
StateProEditorProps["universeTemplates"] |
libraryBehaviors |
StateProEditorProps["libraryBehaviors"] |
features |
StateProEditorProps["features"] |
onChange |
StateProEditorProps["onChange"] |
onLocaleChange |
StateProEditorProps["onLocaleChange"] |
| Event | Detail |
|---|---|
studio-change |
StudioChangePayload |
studio-locale-change |
{ locale: StudioLocale } |
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from "vue";
import {
defineStateProStudioElement,
STUDIO_CHANGE_EVENT,
STUDIO_LOCALE_EVENT,
} from "@rendis/statepro-studio-web-component";
import type { StudioChangePayload, StudioExternalValue } from "@rendis/statepro-studio-react";
import "@rendis/statepro-studio-react/styles.css";
defineStateProStudioElement();
const studioEl = ref<HTMLElement | null>(null);
const initialValue: StudioExternalValue = {
definition: {
id: "machine-id",
canonicalName: "machine",
version: "1.0.0",
universes: {},
},
};
const handleChange = (event: Event) => {
const payload = (event as CustomEvent<StudioChangePayload>).detail;
console.log("studio-change", payload);
};
const handleLocale = (event: Event) => {
const detail = (event as CustomEvent<{ locale: "en" | "es" }>).detail;
console.log("studio-locale-change", detail.locale);
};
onMounted(() => {
const el = studioEl.value as
| (HTMLElement & {
value?: StudioExternalValue;
features?: unknown;
})
| null;
if (!el) return;
el.value = initialValue;
el.features = {
json: { import: true, export: true },
library: {
behaviors: { manage: false },
metadataPacks: { create: false },
},
performance: {
mode: "auto",
},
};
el.addEventListener(STUDIO_CHANGE_EVENT, handleChange as EventListener);
el.addEventListener(STUDIO_LOCALE_EVENT, handleLocale as EventListener);
});
onBeforeUnmount(() => {
const el = studioEl.value;
if (!el) return;
el.removeEventListener(STUDIO_CHANGE_EVENT, handleChange as EventListener);
el.removeEventListener(STUDIO_LOCALE_EVENT, handleLocale as EventListener);
});
</script>
<template>
<statepro-studio
ref="studioEl"
style="display: block; width: 100%; height: 100vh"
/>
</template>If your Vue build complains about unknown custom elements, configure compiler custom element handling for statepro-studio.
| Prop | Type | Default | Behavior |
| -------------------- | ---------------------------------------- | -------------------------- | -------------------------------------------------------- | ------------------------ |
| value | StudioExternalValue | undefined | Controlled mode source of truth. |
| defaultValue | StudioExternalValue | undefined | Initial value for uncontrolled mode. |
| onChange | (payload: StudioChangePayload) => void | undefined | Called after debounced serialization/validation updates. |
| changeDebounceMs | number | 250 | Debounce window for onChange. |
| universeTemplates | StudioUniverseTemplate[] | [] | Enables template-based universe creation in toolbar. |
| libraryBehaviors | BehaviorRegistryItem[] | undefined | External behavior catalog merged with built-ins and user entries. |
| features | StudioFeatureFlags | JSON/library enabled + performance auto | Enables/disables capabilities and configures adaptive performance mode. |
| locale | "en" | "es" | undefined | Controlled locale value. |
| defaultLocale | "en" | "es" | "en" | Initial/fallback locale. |
| onLocaleChange | (locale: StudioLocale) => void | undefined | Notified when locale changes in UI/provider. |
| persistLocale | boolean | true | Writes/reads locale from localStorage. |
| showLocaleSwitcher | boolean | true | Shows/hides header language toggle button. |
- Studio always registers runtime built-ins (
builtin:*) in the library. libraryBehaviorsdoes not replace the registry; it is merged as:built-ins + external + user.- Built-ins cannot be deleted and keep official
src,type, anddescription. - Built-ins are fully read-only in Library modal (including
simScript). - If an external entry uses the same
srcas a built-in, Studio keeps built-in identity/description and only takes externalsimScript.
features?: {
json?: {
import?: boolean;
export?: boolean;
};
library?: {
behaviors?: {
manage?: boolean;
};
metadataPacks?: {
create?: boolean;
};
};
performance?: {
mode?: "auto" | "off" | "aggressive";
staticPressureThreshold?: number;
onEmaMs?: number;
offEmaMs?: number;
onMissRatio?: number;
offMissRatio?: number;
};
}| Flag | true |
false |
|---|---|---|
json.import |
Enables model/layout import tab/actions in JSON modal | Import actions disabled/hidden |
json.export |
Enables model/layout export tab/actions in JSON modal | Export actions disabled/hidden |
library.behaviors.manage |
Allows creating/editing/deleting behavior registry items | Behavior management UI/actions disabled |
library.metadataPacks.create |
Allows creating new metadata packs | New pack creation is disabled |
Performance flags:
| Flag | Default | Effect |
|---|---|---|
performance.mode |
auto |
auto: adaptive, off: no adaptive degradation, aggressive: enables earlier/longer. |
performance.staticPressureThreshold |
1200 |
Static activation threshold using renderPressure = nodeCount + routeSegmentCount*2 + transitionCount*8. |
performance.onEmaMs |
18 |
Turns on adaptive mode when frame-time EMA crosses this value during interaction. |
performance.offEmaMs |
14 |
Turns adaptive mode off (with hysteresis) when EMA goes below this value. |
performance.onMissRatio |
0.25 |
Turns on adaptive mode when ratio of frames above 16.7ms crosses this value. |
performance.offMissRatio |
0.1 |
Turns adaptive mode off (with hysteresis) when miss ratio drops below this value. |
In auto/aggressive, Studio may temporarily cull offscreen nodes/transitions and reduce non-critical overlays during heavy interaction, then restore full fidelity when load drops.
Important nuance:
library.metadataPacks.create = falsedisables creating new packs.- Existing metadata packs can still be opened/edited by current implementation.
onChange is emitted with debounce (changeDebounceMs, default 250).
Source semantics:
source: "user": user-driven edits inside Studio (graph, properties, library, import, etc.).source: "external-sync": controlled re-synchronization after parent updatesvalue(or external behavior registry sync in uncontrolled behavior-list updates).
Recommended controlled persistence pattern:
const handleChange = (payload: StudioChangePayload) => {
if (payload.source === "external-sync") {
return; // prevents persistence loops
}
saveToServer(payload.machine, payload.layout);
setValue({
definition: payload.machine,
layout: payload.layout,
metadataPacks: {
registry: payload.layout.packs.packRegistry,
bindings: payload.layout.packs.bindings,
},
});
};Supported locales:
enes
Locale-related props:
locale: controlled locale.defaultLocale: fallback locale.onLocaleChange: callback on locale update.persistLocale: enables storage persistence.showLocaleSwitcher: toggles language switch UI.
Persistence behavior:
- When
persistLocaleis enabled, Studio stores locale inlocalStorageusing keystatepro.studio.locale. - On startup, if
localeis not controlled, Studio resolves initial locale from storage (when available) and falls back todefaultLocale.
| Mode | How to Use | Notes |
|---|---|---|
| Controlled | Provide value and update it from onChange |
Parent owns full source of truth |
| Uncontrolled | Omit value, optionally provide defaultValue |
Studio owns internal state after initialization |
Guidelines:
- Use controlled mode if you need autosave, collaborative sync, or authoritative external persistence.
- Use uncontrolled mode for simpler embedded editing where parent does not need full state synchronization on every change.
Validation model:
- Studio serializes editor state to
StateProMachinecontinuously. - Validation output is exposed in
onChange.payload.issues. canExportindicates whether blocking errors exist.
Import/Export behavior:
- JSON model import validates machine schema/semantics before applying.
- Layout import validates layout structure before applying.
- Feature flags (
json.import/json.export) can hide/disable tabs and actions.
Issue interpretation:
severity: "error": blocks export/import actions that require valid payloads.severity: "warning": non-blocking but should be reviewed.
Cause:
@rendis/statepro-studio-react/styles.cssnot imported (scrollbar styles missing).- Tailwind pipeline not active in host app.
- Tailwind v3:
contentglobs missing Studio classes. - Tailwind v4:
@sourcepath incorrect or not resolving (check relative path from CSS file, verify symlinks in monorepos). - Tailwind v4:
@themeblock with dynamic values (hsl(var(...))) instead of@theme inline— removes default color palette. - Host app global CSS (
* { border-color: ... }) overriding Studio borders.
Fix:
- Import
@rendis/statepro-studio-react/styles.cssor copy scrollbar CSS. - Ensure PostCSS + Tailwind are configured.
- Tailwind v3: add correct
contentglobs (node_modules/@rendis/statepro-studio-reactor workspace source paths). - Tailwind v4: verify
@sourcepath resolves to the actualdist/directory. Use@theme inlinefor dynamic color values. - Wrap Studio in an isolation container to reset inherited styles (see "Host App CSS Interference" above).
Cause:
- Parent writes back
valuefor every callback, includingsource: "external-sync"payloads.
Fix:
- Ignore
external-syncupdates in persistence and state-rewrite logic.
Cause:
- Schema or semantic validation errors in model/layout.
Fix:
- Inspect
issuesdetails (field,message,severity). - Resolve all
errorentries first. - Re-try import after fixing blocking fields.
cd studio
pnpm typecheck
pnpm test
pnpm build