@process.co/ui
v0.0.24
Published
Process.co UI Component Library
Downloads
767
Readme
@process.co/ui
A React UI component library for Process.co applications with built-in collaborative editing, type inference, and expression support.
Installation
npm install @process.co/uiImporting Styles
import '@process.co/ui/styles';Field Components
The Input and Select components are collaborative-aware form controls designed for building custom UIs that integrate with Process.co's flow editor.
Quick Start
import { Input, Select, useNodeProperty } from '@process.co/ui/fields';
import '@process.co/ui/styles';
function MyCustomControl({ fieldName }) {
const [value, setValue] = useNodeProperty(fieldName);
return (
<Input
fieldName="expression"
label="Expression"
expectedType="string"
value={value}
onChange={setValue}
/>
);
}Input Component
A text input with expression support, type inference, and real-time collaboration.
Props
| Prop | Type | Description |
|------|------|-------------|
| fieldName | string | Unique identifier for collaborative sync |
| label | string | Display label |
| value | any | Current value |
| onChange | (value: any) => void | Change handler |
| expectedType | string | Expected type for validation (see Type Inference) |
| placeholder | string | Placeholder text |
Example
<Input
fieldName="userEmail"
label="Email Address"
expectedType="string"
value={email}
onChange={setEmail}
placeholder="[email protected]"
/>Select Component
A dropdown select with expression support and type-aware options.
Props
| Prop | Type | Description |
|------|------|-------------|
| fieldName | string | Unique identifier for collaborative sync |
| label | string | Display label |
| value | any | Current selected value |
| onChange | (value: any) => void | Change handler |
| options | SelectOption[] | Available options |
| expectedType | string | Expected type for validation |
SelectOption Type
type SelectOption = {
value: string;
label: string;
node?: ReactNode; // Custom render
};Example
<Select
fieldName="operator"
label="Operation"
value={operator}
onChange={setOperator}
options={[
{ value: 'equals', label: 'Equals' },
{ value: 'contains', label: 'Contains' },
{ value: 'startsWith', label: 'Starts With' },
]}
/>Type Inference System
The $infer<...> syntax enables automatic type inference and propagation between fields.
How It Works
When a field uses $infer or $infer<allowedTypes> as its expectedType:
- The field infers the type of the user's input
- The field publishes that inferred type under its
fieldName - Other fields can subscribe to that type using
$infer<[fieldName]>
Publishing Types (Automatic)
Any field with $infer syntax automatically publishes its inferred type:
// This field publishes its inferred type as "switchExpression"
<Input
fieldName="switchExpression"
expectedType="$infer<string | number | boolean>"
/>The field will:
- Accept string, number, or boolean values
- Infer the actual type from user input (e.g., if user types
"hello", infersstring) - Publish the inferred type so other fields can access it via
fieldName
You can also use just $infer without constraints:
// Publishes inferred type with no restrictions on allowed types
<Input
fieldName="myExpression"
expectedType="$infer"
/>Or with a single known type:
// Publishes "string" as the inferred type for this field
<Input
fieldName="stringField"
expectedType="$infer<string>"
/>Subscribing to Types
A field can subscribe to another field's published type:
// This field receives its expected type from "switchExpression"
<Input
fieldName="caseValue"
expectedType="$infer<[switchExpression]>"
/>This field will:
- Look up the inferred type published by
switchExpression - Use that type for validation and autocomplete
- Update automatically when the source field's type changes
Multi-Field Subscription
Subscribe to multiple fields - types are intersected:
<Input
fieldName="value"
expectedType='$infer<["statementField", "operatorField"]>'
/>The expected type is computed by intersecting types from both fields.
Example Flow
// 1. Statement field publishes its inferred type
<Input
fieldName="statement"
expectedType="$infer<string | number | boolean>"
// User enters: this.user.age
// Infers and publishes: "number"
/>
// 2. Operator dropdown reads the published type to filter options
const ctx = useInferredTypes();
const statementType = ctx?.getInferredType('statement'); // "number"
const operators = statementType === 'number'
? ['=', '!=', '<', '>', '<=', '>=']
: ['=', '!='];
// 3. Value field subscribes to the statement's type
<Input
fieldName="value"
expectedType="$infer<[statement]>"
// Expects: "number" (from statement field)
/>useInferredTypes Hook
Access inferred types programmatically for building type-aware UIs:
import { useInferredTypes } from '@process.co/ui/fields';
function OperatorSelect() {
const ctx = useInferredTypes();
// Read the published type from another field
const expressionType = ctx?.getInferredType('switchExpression') || 'any';
// Manually publish a type (e.g., for operator narrowing)
ctx?.setInferredType('operatorNarrow', 'string');
// Filter operators based on type
const operators = expressionType === 'number'
? ['=', '!=', '<', '>', '<=', '>=']
: expressionType === 'string'
? ['=', '!=', 'contains', 'startsWith', 'endsWith']
: ['=', '!='];
return (
<Select options={operators.map(op => ({ value: op, label: op }))} />
);
}Context Methods
| Method | Description |
|--------|-------------|
| getInferredType(fieldName) | Get the published type for a field |
| setInferredType(fieldName, type) | Manually publish a type for a field |
| clearInferredType(fieldName) | Remove a published type for a field |
| clearAllInferredTypes() | Remove all published types |
| inferredTypes | Record of all fieldName → type mappings |
Cleanup on Unmount
When building controls that publish inferred types, clean up on unmount to avoid stale types:
useEffect(() => {
// Set the inferred type
ctx?.setInferredType(myFieldName, computedType);
// Cleanup on unmount
return () => {
ctx?.clearInferredType?.(myFieldName);
};
}, [myFieldName, computedType]);useNodeProperty Hook
Subscribe to and update node properties with automatic collaboration sync:
import { useNodeProperty } from '@process.co/ui/fields';
function MyControl({ fieldName }) {
const [value, setValue] = useNodeProperty<MyValueType>(fieldName);
// value: Current property value (undefined if not set)
// setValue: Update function with automatic sync
return (
<button onClick={() => setValue({ ...value, enabled: true })}>
Enable
</button>
);
}Flow Editor Actions
Custom controls can trigger flow-level side effects using a unified hook:
import { useFlowEditorActions } from '@process.co/ui/fields';
function SwitchEditor({ fieldName }) {
const { triggerLayoutUpdate, triggerValidation, clearValidationErrorsByPrefix } =
useFlowEditorActions();
const handleRemoveCase = (caseId: string) => {
clearValidationErrorsByPrefix(caseId);
triggerLayoutUpdate();
triggerValidation();
};
}Actions
| Action | Description |
|--------|-------------|
| triggerLayoutUpdate() | Recalculate node layout after structural changes |
| triggerValidation() | Run unified flow validation |
| clearValidationErrorsByPrefix(prefix) | Remove expression errors matching a field prefix |
Slots
Slots are logical sub-containers or branches used in flow control. Elements like Switch or If-Then expose multiple slots (e.g. cases or branches); each slot can contain child steps. The flow editor and property panel abstract away layout and evaluation so custom controls only need to work with slot IDs and active/enabled state.
What slots are
- Container elements (e.g. Switch, If-Then, Loop) define a set of slots. Each slot is one branch or case.
- At runtime, the active slot is the branch that evaluation chose (e.g. the matching case in a Switch).
- Slots can be enabled or disabled per branch; the host evaluates this from your element data.
- Custom controls receive slot context via
useSlotContext(slotId)and can use slot UI components to render branch lists, enable/disable toggles, reorder/delete, and manage per-slot exports.
useSlotContext
Use this hook when your control needs to know whether a given slot is active (evaluated path) or enabled (user can toggle).
import { useSlotContext } from '@process.co/ui/slots';
function MyBranchLabel({ slotId }: { slotId: string }) {
const slot = useSlotContext(slotId);
if (!slot) return null; // Not inside a provider
return (
<span className={slot.active ? 'font-bold' : ''}>
{slot.enabled ? 'On' : 'Off'}
</span>
);
}Returns: { active: boolean, enabled: boolean } | undefined
| Property | Description | |----------|-------------| | active | Whether this slot is the one evaluation selected (e.g. the current case). | | enabled | Whether the slot is enabled (from the element’s slot definition). | | undefined | When not inside a host that provides slot context (e.g. outside the flow editor property panel). |
Slot UI components
These components work with the host’s TemplateFieldProvider context and are intended for building branch/case UIs:
| Component | Description |
|-----------|-------------|
| SlotElements | Renders the list of elements in a slot (e.g. steps in a case). |
| SlotEnable | Toggle to enable/disable a slot (uses context isSlotEnabled / handleSlotEnabledChange). |
| SlotDelete | Control to delete a slot or its contents. |
| SlotDragHandle | Drag handle for reordering within a slot. |
| ExportManager | Button or full form for managing how a slot’s result is exported (mapping or code). Opens the host’s export editor pane when clicked. |
When running inside the flow editor, the host wires slot state and callbacks; you do not pass slot state yourself. In Storybook or element-dev-server, these components render as stubs or use dev context where available.
ExportManager
ExportManager lets users configure how a slot’s output is exported (e.g. mapping paths or custom code). You render one per slot; the host opens a shared “Manage exports” pane when the user clicks the button.
Props
| Prop | Type | Description |
|------|------|-------------|
| slotId | string | Required. Slot identifier (e.g. case id). Use the same id your UI uses for this branch. |
| variant | 'button' \| 'full' | 'button' shows only a “Manage … exports” button; 'full' shows the full form (used inside the host’s pane). Default 'button'. |
| embeddedInPanel | boolean | When true, the form is always expanded (e.g. inside the host’s pane). |
| slotLabel | string | Slot type label (e.g. "case", "path", "slot"). Used in the pane header: “Manage {slotLabel} exports: {slotName}”. Default from context or "slot". |
| slotName | string \| React.ReactNode | Slot instance name (e.g. “Case 1”, “Path A”). You compose this to match your UI; it appears after the colon in the pane header. Supports ReactNode for formatting (e.g. bold, icons). Fallback in the pane is slotId. |
| exportPlaceholder | string \| React.ReactNode | Placeholder for the export code editor (e.g. "return { ... };"). Passed to the pane when the editor opens. Supports ReactNode for formatted hints. |
Example
import { ExportManager } from '@process.co/ui/slots';
// One button per case; when clicked, opens the host’s export pane with a clear header
<ExportManager
slotId={case.id}
slotLabel="case"
slotName={case.label ?? `Case ${index + 1}`}
exportPlaceholder="return { outcome: steps.lastStep.results };"
/>The pane header will show: Manage case exports: {slotName}. The host provides the full export form (mapping rules or code) in the pane; your props only customize the labels and placeholder.
Slot definition (for element authors)
If you define an action or signal with branches/cases, you declare slots in the element definition. The host uses this to build the slot list, resolve the active slot from evaluation results, and wire enable/disable. You do not implement layout or evaluation yourself.
- Static slots – Fixed set of branches (e.g. "then" and "else"). In the definition:
type: 'static', anid(optional template like{{ID_GUID}}_default_case), and optionallabelPath,labelValue,enabledPath. - Dynamic slots – Branches come from an array in your element data (e.g.
cases[]). In the definition: apath(e.g.$.data.cases.cases[*]), plusidPath,labelPath, and optionallyenabledPathper item. - Active slot – The host evaluates your element and reads the chosen branch from the evaluation result. You provide paths in the definition (e.g.
activeSlotId,activeSlotLabel) that point into that result; the host uses them to driveisSlotActiveand the canvas.
The exact schema (e.g. slots.slots[], slots.activeSlotId) is defined by the host and the namespace/action APIs. As a custom control author, you only need useSlotContext(slotId) and the slot components; the host wires definitions to context.
Building Custom Controls
Custom controls integrate with Process.co's collaborative editing system through the useNodeProperty hook.
Basic Pattern
import { useNodeProperty, useInferredTypes } from '@process.co/ui/fields';
interface MyControlProps {
fieldName: string;
readonly?: boolean;
}
export default function MyControl({ fieldName, readonly = false }: MyControlProps) {
// Subscribe to the property value
const [value, setValue] = useNodeProperty<MyValueType>(fieldName);
// Access type inference context (optional)
const ctx = useInferredTypes();
// Derive state from value
const items = value?.items ?? [];
// Update handler
const addItem = () => {
setValue({
...value,
items: [...items, { id: generateId(), name: '' }]
});
};
return (
<div>
{items.map(item => (
<ItemRow key={item.id} item={item} />
))}
{!readonly && <button onClick={addItem}>Add Item</button>}
</div>
);
}With Type Inference
import {
useNodeProperty,
useInferredTypes,
Input,
Select,
} from '@process.co/ui/fields';
export default function ConditionalEditor({ fieldName }) {
const [value, setValue] = useNodeProperty(fieldName);
const ctx = useInferredTypes();
// Get inferred type from statement field (it publishes automatically)
const statementType = ctx?.getInferredType(`${fieldName}_statement`) || 'any';
// Filter operators based on statement type
const operators = getOperatorsForType(statementType);
return (
<div>
{/* This field PUBLISHES its inferred type as "${fieldName}_statement" */}
<Input
fieldName={`${fieldName}_statement`}
label="Statement"
expectedType="$infer<string | number | boolean>"
value={value?.statement}
onChange={(v) => setValue({ ...value, statement: v })}
/>
<Select
fieldName={`${fieldName}_operator`}
label="Operator"
options={operators}
value={value?.operator}
onChange={(v) => setValue({ ...value, operator: v })}
/>
{/* This field SUBSCRIBES to the statement field's type */}
<Input
fieldName={`${fieldName}_value`}
label="Value"
expectedType={`$infer<[${fieldName}_statement]>`}
value={value?.value}
onChange={(v) => setValue({ ...value, value: v })}
/>
</div>
);
}Operator Utilities
Shared utilities for building query builders with type-aware operators.
Types
import {
BaseOperatorType,
OperatorDef,
ParsedTypes,
} from '@process.co/ui/fields';
// BaseOperatorType includes: 'exists', 'not_exists', 'string_equals',
// 'string_contains', 'number_gt', 'boolean_equals', etc.
// OperatorDef<T> is generic - extend with custom operators
type MyOperator = 'expression' | 'custom_check';
const operators: OperatorDef<MyOperator>[] = [...];Functions
import {
parseInferredTypes,
computeExtendedType,
filterOperatorsByType,
getStringConstants,
getNumberConstants,
} from '@process.co/ui/fields';
// Parse type string into components
const parsed = parseInferredTypes('"adam" | "beth" | number');
// { baseTypes: ['string', 'number'], stringConstants: ['adam', 'beth'], ... }
// Filter operators by compatible type
const ops = filterOperatorsByType(OPERATORS, '"adam" | "beth"');
// Returns operators with types: ['any'] or ['string']
// Compute extended type for operators with extendsWithBase
const expectedType = computeExtendedType('"adam" | "beth"', operatorDef);
// If extendsWithBase: true, returns '"adam" | "beth" | string'Extended Type Narrowing
Some operators support extendsWithBase for flexible type matching:
const OPERATORS: OperatorDef[] = [
// Exact match - only accepts the literal values
{ value: 'string_equals', narrowsTo: 'string', extendsWithBase: false },
// Extended match - accepts literals OR any string
{ value: 'string_starts_with', narrowsTo: 'string', extendsWithBase: true },
];Example: If statement infers "adam" | "beth":
string_equalsexpects:"adam" | "beth"(must match exactly)string_starts_withexpects:"adam" | "beth" | string(can provide partial match like"a")
Collaboration Features
All field components support real-time collaboration when used within the Process.co flow editor:
- Cursor sharing: See where other users are editing
- Conflict resolution: Automatic handling of concurrent edits via Yjs
- Presence indicators: Visual indication of active editors
These features work automatically when components are rendered within a NodePropertyProvider context.
UI Components
The library also includes standard UI components:
import {
Button,
Card,
Alert,
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
ConfirmationDropdownMenuItem,
} from '@process.co/ui';See the Storybook documentation for full component reference.
