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

@istar-ts/react

v0.12.0

Published

React components for editing iStar 2.0 goal models, built on React Flow with a pluggable element registry.

Readme

@istar-ts/react

React components for editing iStar 2.0 goal models: a React Flow–based canvas, palette, inspector, and a registry that lets you override how each element kind renders, its default properties, inspector form, and palette entry. Pair with @istar-ts/core for load/save, constraints, and typed customProperties.

pnpm add @istar-ts/react @istar-ts/core react react-dom

Import the single stylesheet once (it includes React Flow’s base styles; rules are scoped under .istar-* classes):

import '@istar-ts/react/styles.css';

Peer dependencies: react and react-dom ^18.2.0 or ^19.0.0.

Quick start

Give the canvas an explicit height (it fills its container). Use a store for undo/redo, or controlled model / onChange.

import '@istar-ts/react/styles.css';
import { parsePistar } from '@istar-ts/core';
import { IstarCanvas, IstarInspector, useIstarStore } from '@istar-ts/react';

const text = await fetch('/model.pistar').then((r) => r.text());

export function Editor() {
  const { store } = useIstarStore(() => parsePistar(text));
  return (
    <div style={{ height: '100vh' }}>
      <IstarCanvas store={store} aside={<IstarInspector />} />
    </div>
  );
}

Controlled mode (no separate store in your app state):

import { useState } from 'react';
import type { IstarModel } from '@istar-ts/core';
import { createEmptyModel } from '@istar-ts/core';
import { IstarCanvas } from '@istar-ts/react';

export function Controlled() {
  const [model, setModel] = useState<IstarModel>(() => createEmptyModel());
  return (
    <div style={{ height: 600 }}>
      <IstarCanvas model={model} onChange={setModel} />
    </div>
  );
}

<IstarCanvas> props include registry, readOnly, palette ('left' by default, 'top' or 'bottom' for a piStar-style bar, or false), aside, extensions, controls (zoom buttons), background (dotted grid, off by default: piStar's paper is plain), fitView (on by default; waits until the container has a usable size, so a canvas mounted in a panel that is still opening is fitted once it has room), diagnostics and issues (findings from LSPs, validators and other analysers; see Diagnostics below), diagnosticBadges, onSelectionChange, linkShape ('straight' by default, or 'curved' for Bézier links that leave each node perpendicular to its side), colorMode ('light' or 'dark', see Theming), minimap and panOnShiftScroll.

Controlling the viewport and selection

The canvas owns its React Flow instance, so useReactFlow() isn't available to your app. Pass a ref instead to fit, zoom, reveal, or select elements, e.g. after resizing the container or when an element is selected elsewhere. Viewport methods resolve to true once the viewport has moved (after any animation), or false if they couldn't (for example, an unknown element id).

import { useRef } from 'react';
import type { IstarCanvasHandle } from '@istar-ts/react';

const canvas = useRef<IstarCanvasHandle>(null);

<IstarCanvas
  ref={canvas}
  store={store}
  onSelectionChange={(sel) => {
    /* mirror to a tree view / VS Code host */
  }}
/>;

canvas.current?.fitView({ padding: 0.1, duration: 200 }); // or { nodes: [elementId] }
canvas.current?.centerOn(elementId, { zoom: 1.5, duration: 200 }); // keeps the zoom if omitted
canvas.current?.select({ type: 'element', id: elementId });
canvas.current?.zoomIn();
canvas.current?.zoomOut();

Diagnostics

Diagnostics are findings about elements and links that are not part of the saved model and are never written to disk. Any analyser can publish them: an LSP server, a model checker, a simulation, an LLM critic. Each one is a GoalDiagnostic (from @istar-ts/core, re-exported here). They are anchored by element id, not by text position:

{
  elementId: string;   // an element or link id
  key?: string;        // the custom property it is about
  severity: 'error' | 'warning' | 'info' | 'hint';
  message: string;
  source?: string;     // who published it
  range?: { start: { line, character }; end: { line, character } };
  code?: string;
  data?: unknown;
}

Publish per source to a DiagnosticsStore. Each publish(source, list) replaces only that source's set, as an LSP publishDiagnostics does. The editor then shows the union of all sources, deduplicated by (elementId, key, message) and keeping the worst severity:

const diagnostics = useDiagnosticsStore(); // or createDiagnosticsStore() outside React

// MutRoSe's webview message ({ nodeId, severity, message }), in one line:
diagnostics.publish('mutrose', fromNodeIdDiagnostics(message.diagnostics));
// LSP diagnostics carrying data.elementId (or data.nodeId) and data.key:
diagnostics.publish('goal-language', fromLspDiagnostics(lspDiagnostics));
// Range-anchored ones: map the range to an element once, in the host.
diagnostics.publish(
  'lsp',
  fromLspDiagnostics(list, { elementIdFor: (d) => idAtLine(d.range!.start.line) }),
);
diagnostics.clear('prism'); // one source; clear() for all

<IstarCanvas store={store} diagnostics={diagnostics} />;

diagnostics also accepts a plain GoalDiagnostic[]. Pass severityNumbering: 'vscode' to fromLspDiagnostics for vscode.Diagnostic objects, whose severities count from 0. istar-ts contains no LSP client.

Diagnostics show up in three places:

  • Badges. Default element components and link labels draw a badge with the worst severity; hovering lists the messages with their source. A badge your own component draws (as before 0.12) replaces the default one. Use diagnosticBadges={false} to turn them off.
  • Inspector. The default inspector lists element-level diagnostics at the top, and property diagnostics under that property's row (schema or free-form), matched by key. A key without a row is listed at the top with its key.
  • Custom UI. Components, inspectors and link labelComponents receive diagnostics. useElementDiagnostics(id) returns them for any id. useGoalDiagnostics() returns the merged list, byElement, and publish / clear for producers inside the editor. <DiagnosticList> and <ElementIssuesBadge diagnostics={…} /> are the default building blocks.

The older issues prop ({ id, severity, message }, an ElementIssue) still works. It is the element-level subset of a diagnostic and is merged with the rest. Components that read props.issues (or useIstarEditor().issuesById) see every diagnostic as an issue: hints read as info, and property diagnostics are prefixed with their key.

Property schemas (defineProperties) remain for inspector UX only; they do not replace an external validator such as an LSP.

Editing behaviour

  • Palette: pick an element or link tool, then click again to cancel. Actors, agents, and roles are placed by clicking empty diagram space. Goals, tasks, resources, and qualities are placed by clicking inside an actor, role, or agent.

  • Links and dependencies: with a link tool active, drag from one node to another. Contribution tools preset the contribution value; dependency tools create a dependum of the chosen node kind between depender and dependee.

  • Constraints: invalid connections are blocked. While dragging, a live tooltip shows canLink’s reason; failed connects also show an error notice at the bottom of the canvas.

  • Delete: Backspace or Delete removes the selection (elements cascade; one dependency half removes the whole dependency).

  • Undo / redo: Ctrl+Z / Cmd+Z and Ctrl+Shift+Z / Cmd+Shift+Z (also Ctrl+Y / Cmd+Y for redo) when the diagram has focus; palette Undo/Redo buttons mirror the store.

  • Rename: double-click an element (or an actor) to edit its name inline; new elements start in edit mode. Enter commits, Shift+Enter adds a line break, Escape cancels.

  • Resize: select an intentional element and drag its handles; the size is saved in display.

  • Links: click to select (the inspector shows them), Delete to remove. Links with vertices are drawn as smooth curves, like piStar's.

  • Actors: select an actor, role or agent by its symbol (the circle at the top left), and drag it by the symbol, or by its body while nothing is selected. Clicks on the rest of its boundary don't select it and leave the current selection as it is, and box selection never picks up the whole frame. Alt+click the symbol to collapse or expand the actor, as in piStar. Collapsed actors hide inner elements; dependency links re-anchor on the actor; other links involving hidden nodes are hidden until expand.

  • Navigation: the wheel zooms; Shift + wheel pans sideways (panOnShiftScroll, on by default). On touch screens, pinch with two fingers to zoom, anywhere on the diagram.

Press Escape to clear the active palette tool.

Extensions and the element registry

Out of the box the editor is a plain piStar: defaultRegistry knows the iStar 2.0 elements and links and nothing else. To adapt it to another modeller (one that annotates tasks with costs, treats resources as variables, adds its own inspectors, …) write an extension and pass it to the canvas. The libraries never hardcode any modeller's properties.

<IstarCanvas store={store} extensions={[myExtension]} />

An IstarExtension is a name plus per-kind overrides; several extensions apply in order (applyExtensions(base, extensions) does the same outside React). For each element kind:

| Field | Role | | ------------------- | ---------------------------------------------------------------- | | component | React node view (ElementComponentProps) | | defaultProperties | customProperties preset, or ({ model }) => … when creating | | defaultName | Name when creating (string or ({ model }) => string) | | inspector | Side-panel form, or false to hide | | palette | Toolbar entry (Partial<PaletteEntry>), or false to hide | | properties | PropertySchema from @istar-ts/core — typed fields + defaults | | size | Default width/height (actors: initial boundary) |

For each link kind: palette (array of tool entries or false), inspector, properties. paletteGroups relabels the palette's dropdown groups.

Example: an extension that gives tasks a cost and a priority, shows the cost on the node, and hides Role from the palette.

import {
  DefaultElementComponent,
  IstarCanvas,
  type ElementComponentProps,
  type IstarExtension,
} from '@istar-ts/react';
import { defineProperties, prop } from '@istar-ts/core';
import type { ReactElement } from 'react';

const taskProperties = defineProperties('istar.Task', {
  cost: prop.number({ min: 0, default: 0, label: 'Cost' }),
  priority: prop.enum(['low', 'medium', 'high'] as const, { optional: true, label: 'Priority' }),
});

function CostTask(props: ElementComponentProps): ReactElement {
  const { values } = taskProperties.read(props.element);
  return (
    <div style={{ position: 'relative' }}>
      <DefaultElementComponent {...props} />
      <span style={{ position: 'absolute', right: 4, bottom: -8, fontSize: 10 }}>
        {`cost ${values.cost ?? 0}`}
      </span>
    </div>
  );
}

export const costs: IstarExtension = {
  name: 'costs',
  elements: {
    // The default inspector renders typed fields from `properties`, so no custom form is needed.
    'istar.Task': { properties: taskProperties, component: CostTask },
    'istar.Role': { palette: false },
  },
};

// <IstarCanvas store={store} extensions={[costs]} />

For a fully custom form set inspector, built from InspectorField, PropertyField, CommitText, CustomPropertiesEditor and useTypedProperties. The playground (examples/playground/src/extensions/) has a complete extension with its own inspector.

createRegistry(overrides, base?) builds a standalone registry from the same overrides, for the registry prop. defaultPropertiesFor(registry, kind, model) merges schema defaults with defaultProperties (preset wins on key clashes).

New element and link kinds

An IstarExtension can bring new kinds as well as presentation. Its metamodel part is a core metamodel extension (see @istar-ts/core, "Extending the metamodel"); elements / links say how the kinds look, like piStar-ext's "Shape" and "Kind of Line" fields:

import { ISTAR_2_0, parsePistar } from '@istar-ts/core';
import { IstarCanvas, LINE_DASHES, metamodelWithExtensions, useIstarStore } from '@istar-ts/react';
import type { IstarExtension } from '@istar-ts/react';

const rationalAgents: IstarExtension<string, string> = {
  name: 'rationalAgents',
  metamodel: {
    name: 'rationalAgents',
    elements: [
      { kind: 'rationalAgents.Planning', behavesLike: 'istar.Task', pistarType: 'istar.Planning' },
      { kind: 'rationalAgents.Plan', category: 'node' },
    ],
    links: [
      {
        kind: 'rationalAgents.GeneratesLink',
        label: 'Generates',
        rules: { sources: ['rationalAgents.Planning'], targets: ['rationalAgents.Plan'] },
      },
    ],
  },
  elements: {
    // SVG path data, scaled to the element (no viewBox needed).
    'rationalAgents.Planning': {
      shape: { path: 'M 0 0 L 80 0 L 100 20 L 80 40 L 0 40 L 14 20 Z' },
    },
  },
  links: {
    'rationalAgents.GeneratesLink': {
      line: { dash: LINE_DASHES.dotted, marker: 'm 10,-6 l -10,6 10,6' },
    },
  },
};

const extensions = [rationalAgents];
const metamodel = metamodelWithExtensions(extensions, ISTAR_2_0);

function Editor({ text }: { text: string }) {
  const { store } = useIstarStore(() => parsePistar(text, { metamodel }));
  return <IstarCanvas store={store} extensions={extensions} />;
}
  • Defaults. The canvas completes its registry for the model's metamodel (registryForMetamodel). Every extended kind gets a palette entry after the built-in ones, its label and size, and the default components. Extended node kinds that can be dependums join the Dependency menu.
  • Nodes. shape draws the kind with SVG path data, scaled to the element (pathBounds computes the drawing's box without a DOM). Without a shape, an extended node is drawn as piStar's default node: a dashed box with its «stereotype» (set stereotype to change or hide it). An extended actor kind looks like the actor it behaves like, or is a dashed circle.
  • Links. line sets the dash (LINE_DASHES: piStar-ext's dashed, dotted, continuous) and the target marker (path data, JointJS convention, as piStar-ext stores it). Without one, an extended link is drawn like the kind it behaves like, or as a continuous line with an open arrow.
  • Constraints. The canvas checks connections with the same canLink as @istar-ts/core, so extended rules apply while dragging, and rejected links say why.

The playground's "iStar4RationalAgents" extension (examples/playground/src/extensions/) and the fixtures/extensions/rationalAgents.txt example show it end to end.

Link labels

By default links are labelled as in piStar: Is-A and Participates-In links show their fixed text, and contributions show their value. piStar keeps a link's name only in its properties panel. Here the default inspector edits it as well, and <IstarCanvas linkNames /> also draws it on the canvas.

For anything else, such as piStar-ext's <<stereotype>> {tag = value}, give the link kind a labelComponent, either through an extension or in the registry. It is drawn as HTML at the link's middle, replaces the default labels for that kind, and receives the texts they would show:

import type { IstarExtension, LinkLabelProps } from '@istar-ts/react';

function Stereotyped({ link, labels }: LinkLabelProps) {
  const stereotype = link.customProperties?.stereotype;
  return (
    <span>
      {stereotype && `<<${stereotype}>> `}
      {labels.value ?? labels.fixed}
    </span>
  );
}

const stereotypes: IstarExtension = {
  name: 'stereotypes',
  links: { 'istar.ContributionLink': { labelComponent: Stereotyped } },
};

Extended link kinds (see "New element and link kinds") take a labelComponent the same way. The playground's iStar4RationalAgents demo uses one to draw piStar-ext's extension block on a link.

Label layout and fitting

The default components lay out an element's name as piStar does: wrapped at the element's full width, with 1em lines. Three per-kind settings change that, through an extension or the registry:

  • textBox: the box the label is laid out in, as fractions cut from each side of the element. The default is the whole element. TEXT_BOXES (goal, task, resource, quality, actor) keeps text inside each piStar shape, but changes where names wrap, so it is opt-in.
  • labelHeader(element): small italic lines above the name (e.g. «action», {Id = G1}), fitted together with it and hidden while the name is edited. An extended kind's default «stereotype» is the first header line.
  • labelFit:
    • { mode: 'shrink' } (the default) leaves names that fit untouched. When the text overflows, the font steps down by step (0.1) to minScale (0.7). If it still overflows, it is ellipsized, with the full text as the tooltip. The text refits whenever the element is resized. It never changes the model.
    • 'none' lets text overflow, as piStar does.
    • 'grow' makes the element taller (display.height) to fit its label, but only after the name is edited (including naming a new element), never on load, so opening a file doesn't change its layout.
import type { IstarExtension } from '@istar-ts/react';
import { TEXT_BOXES } from '@istar-ts/react';

const stereotypes: IstarExtension = {
  name: 'stereotypes',
  elements: {
    'istar.Task': {
      textBox: TEXT_BOXES.task,
      labelHeader: (task) => {
        const s = task.customProperties?.stereotype;
        return s ? [`«${s}»`] : null;
      },
    },
  },
  links: {
    'istar.ContributionLink': {
      labelHeader: (link) =>
        link.customProperties?.tag ? [`{${link.customProperties.tag}}`] : null,
    },
  },
};

Links take a labelHeader too: its lines are drawn above the default label, so annotations need no labelComponent. A labelComponent receives them as labels.header. For your own components, useFitText(ref) fits the first child of ref inside it and returns the font scale, and <LabelHeader lines={…} /> draws header lines in the default style.

Palette

Palette entries carry an icon (the default registry draws piStar-like previews, exported as elementIcon, linkIcon and dependencyIcon), an order, a section (sections are separated by a divider) and an optional group: entries sharing a group collapse into one button with a ▾ menu, as piStar does for Actor, Actor links, Dependency and Contribution. While a tool is active a status hint (the entry's title) says what to do next.

<IstarPalette orientation="vertical" | "horizontal" flyout="right" | "below" | "above" showLabels history /> can also be placed yourself inside an <IstarProvider> (use <IstarCanvas palette={false}> then).

Several entries for one kind. An element kind's palette can be a list, like a link kind's: each entry is its own tool creating that kind, optionally presetting properties (merged over defaultProperties). Entries inherit the kind's icon, section and order unless they set their own, and entries sharing a group collapse into one dropdown:

const extension: IstarExtension = {
  name: 'typed-resources',
  elements: {
    'istar.Resource': {
      palette: [
        { label: 'Boolean', group: 'resource', properties: { type: 'bool' } },
        { label: 'Integer', group: 'resource', properties: { type: 'int' } },
      ],
    },
  },
  paletteGroups: { resource: { label: 'Resource' } },
};

Your own tool bar. With palette={false}, render any controls you like (e.g. as the canvas aside) and drive the editor's tools yourself. usePaletteControls() returns the registry's entries as data (label, icon, active, select(), plus undo/redo), or build tools directly with useIstarEditor().setTool(tool). An element tool can preset custom properties on what it creates:

function MyBar() {
  const { tool, setTool } = useIstarEditor();
  const achieve = {
    type: 'element',
    kind: 'istar.Goal',
    properties: { GoalType: 'Achieve' },
  } as const;
  return <button onClick={() => setTool(achieve)}>Achieve</button>;
}

<IstarCanvas store={store} palette={false} aside={<MyBar />} />;

Actors without a boundary. An actor kind with boundary: false (e.g. in an extension) is drawn as its component alone, at the kind's size, instead of a boundary around its elements. The elements stay nested in the model and move with the actor; nodes added on empty canvas space join the nearest such actor.

Reusable pieces: DefaultElementComponent, DefaultActorComponent, EditableLabel, and shape primitives (GoalShape, ResourceShape, TaskShape, QualityShape, ActorSymbol). Export helpers: defaultRegistry, elementSize, modelToFlow, actorBoundary.

Default inspectors (DefaultElementInspector, DefaultLinkInspector) use PropertyField and CustomPropertiesEditor when you omit a custom inspector.

Composition

Split the UI with one shared editor context:

import {
  IstarCanvas,
  IstarInspector,
  IstarPalette,
  IstarProvider,
  useIstarStore,
} from '@istar-ts/react';

function Layout() {
  const { store } = useIstarStore();
  return (
    <IstarProvider store={store}>
      <header>
        <IstarPalette />
      </header>
      <main style={{ display: 'flex', height: 'calc(100vh - 48px)' }}>
        <div style={{ flex: 1, minWidth: 0 }}>
          <IstarCanvas palette={false} />
        </div>
        <IstarInspector />
      </main>
    </IstarProvider>
  );
}

useIstarEditor() exposes store, model, registry, readOnly, tool / setTool, selection / select, editingId, notify / notice, checkConnection, and elementActions / linkActions. useSelectedTarget() returns the selected element or link. useStoreModel(store) subscribes to any ModelStore.

<IstarCanvas> wraps itself in IstarProvider when none exists; with an outer provider, pass store or model / onChange on the provider instead.

Theming

Override CSS custom properties on .istar-canvas or any ancestor of the editor:

| Variable | Default | Used for | | -------------------------- | ----------------------- | ----------------------- | | --istar-font-family | Arial, Helvetica… | UI type | | --istar-font-size | 12px | Base size | | --istar-text | #000 | Text | | --istar-stroke | #000 | Shapes and links | | --istar-node-stroke | var(--istar-stroke) | Node outlines | | --istar-node-text | #000 | Element names | | --istar-link-stroke | var(--istar-stroke) | Links and markers | | --istar-boundary-stroke | var(--istar-stroke) | Actor boundaries | | --istar-stroke-width | 2px | Node outlines | | --istar-link-width | 1px | Link lines | | --istar-node-fill | rgb(205, 254, 205) | Inner elements | | --istar-actor-fill | rgb(242, 242, 242) | Actor boundary | | --istar-selection | #2f6fe4 | Selection, focus | | --istar-canvas-bg | #fff | Diagram background | | --istar-panel-bg | #fafafa | Palette and inspector | | --istar-panel-border | #d9d9d9 | Panel borders | | --istar-button-bg | #fff | Buttons | | --istar-button-active-bg | #dfe9fc | Active palette tool | | --istar-button-hover-bg | rgba(0, 0, 0, 0.06) | Hovered palette tool | | --istar-palette-bg | var(--istar-panel-bg) | Palette background | | --istar-palette-divider | #dcdcdc | Palette section divider | | --istar-error-bg | #fdecea | Errors, connection hint | | --istar-error-text | #8a1c1c | Error text | | --istar-info-bg | #eef4fd | Info notices | | --istar-info-text | #1d3f7a | Info text |

colorMode="dark" applies a dark theme (the istar-dark class, which also works on an ancestor of a standalone palette or inspector) and React Flow's dark controls and minimap. Nodes keep their light fills with dark outlines and names; the same variables override it.

.my-app .istar-canvas {
  --istar-node-fill: #e8f4ff;
  --istar-selection: #0d9488;
  --istar-canvas-bg: #f8fafc;
}

Credits

Element shapes and editing behaviour are derived from piStar by João Pimentel and contributors, released under the MIT License. The canvas uses React Flow (@xyflow/react, MIT).