npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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/ui

Importing 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:

  1. The field infers the type of the user's input
  2. The field publishes that inferred type under its fieldName
  3. 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:

  1. Accept string, number, or boolean values
  2. Infer the actual type from user input (e.g., if user types "hello", infers string)
  3. 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:

  1. Look up the inferred type published by switchExpression
  2. Use that type for validation and autocomplete
  3. 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', an id (optional template like {{ID_GUID}}_default_case), and optional labelPath, labelValue, enabledPath.
  • Dynamic slots – Branches come from an array in your element data (e.g. cases[]). In the definition: a path (e.g. $.data.cases.cases[*]), plus idPath, labelPath, and optionally enabledPath per 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 drive isSlotActive and 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_equals expects: "adam" | "beth" (must match exactly)
  • string_starts_with expects: "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.