Skip to content

Latest commit

 

History

460 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

tetrascience-react-ui

React component library for building TetraScience applications.

npm version CI Storybook | Contributing Guide

Version

v1.0.0

This library provides:

  • UI Components: shadcn/ui primitives (Radix UI) with Tailwind CSS
  • Composed Components: TetraScience-specific compositions (AppHeader, Sidebar, etc.)
  • Data Visualisation: Interactive charts powered by Plotly.js
  • Theming: CSS custom properties (oklch) for light/dark mode
  • TypeScript: Full type support with exported prop types

Requirements

  • React 19+
  • Node.js 18+
  • TypeScript 5.5+ (optional, but recommended)

Compatibility

Library version React Node.js TDP (server utilities)
v1.0.x 19+ 18+ v4.x+
v0.7.x 19+ 18+ v4.x+
v0.6.x 19+ 18+ v4.x+
v0.5.x 19+ 18+ v4.x+
v0.4.x 19+ 18+ v4.x+

Note: The client-side components have no TDP version dependency. The /server utilities (JWT auth, provider helpers) require a running TDP instance of v4.x or later. Browser support follows React 19's matrix (modern evergreen browsers).

As of v1.0.0, heavy dependencies are optional peer dependencies — see Optional peer dependencies below for what to install and when. Upgrading from v0.7.x? Chart components were renamed and four components were removed: read the v0.7.x → v1.0.0 migration guide first.

Installation

yarn add @tetrascience-npm/tetrascience-react-ui

Optional peer dependencies

The kit does not install heavy dependencies for you. Add only the ones your app uses:

You use… Install
Any charts/ component plotly.js-dist
MessageResponse / Reasoning (AI markdown) @streamdown/math, @streamdown/mermaid
MoleculeStructure @rdkit/rdkit — plus a served WASM, see below
Any import from the package root @streamdown/math, @streamdown/mermaid — see the caveat below
/server Athena / Snowflake / Databricks provider @aws-sdk/client-athena / snowflake-sdk / @databricks/sql

Root-entry imports pull in the streamdown peers whether or not you use them. The AI markdown plugins are loaded through a dynamic import, but a dynamic-import target is still part of your bundler's module graph and its named static imports must resolve. With @streamdown/math absent, a root-entry build fails even when your only kit import is AreaPlot:

dist/components/ai/streamdown-plugins.js (2:9): "math" is not exported by
"__vite-optional-peer-dep:@streamdown/math:@tetrascience-npm/tetrascience-react-ui"

Install the two packages, or use per-component imports, which avoid the barrel entirely. This fails at build time, so it can never reach production unnoticed. Tracked in SW-2472.

A missing plotly.js-dist behaves differently: it does not fail the build under Vite/Rollup — it resolves to an empty stub and the chart fails at runtime with a console error from the loader (Failed to load 'plotly.js-dist' …). If your charts render blank after upgrading, check this first.

Quick Start

// 1. Import the CSS once at your app root (required)
import "@tetrascience-npm/tetrascience-react-ui/index.css";

// 2. Import components
import { Button, Card, CardHeader, CardContent } from "@tetrascience-npm/tetrascience-react-ui";

function App() {
  return (
    <Card>
      <CardHeader>Welcome</CardHeader>
      <CardContent>
        <p>My first TetraScience app!</p>
        <Button variant="default">Get Started</Button>
      </CardContent>
    </Card>
  );
}

Only need a handful of components? Every one is also importable individually — see Per-Component Imports below.

Per-Component Imports

Every component is also reachable at its own subpath, grouped by category: ui/*, composed/*, charts/*, ai/*, utils/*:

import { Button } from "@tetrascience-npm/tetrascience-react-ui/ui/button";
import { StatCard } from "@tetrascience-npm/tetrascience-react-ui/composed/StatCard";
import { AreaPlot } from "@tetrascience-npm/tetrascience-react-ui/charts/AreaPlot";

Importing this way only pulls in that component's own module graph — the main @tetrascience-npm/tetrascience-react-ui import still works exactly as before and pulls in everything. The difference matters most for Jest, which has no tree-shaking and re-evaluates the full import graph on every test file: a full-barrel import costs ~1.2s of module evaluation per test file; a single-component subpath costs ~0.1s. For a production bundler (Vite, webpack 5) the difference is smaller since unused components are already tree-shaken from the main import.

The subpath name always matches the component's directory/file under src/components/<category>/ — check DESIGN.md or the Storybook sidebar for the exact name.

Known gap in v1.0.0: ./ui/progress and ./ui/snippet resolve their types but ship no runtime module, so importing either typechecks cleanly and then fails your build. Neither component is exported from the package root either, so nothing regressed — but the subpath makes them look available. Don't import them. Tracked in SW-2472.

Styling & CSS

This library uses Tailwind CSS 4 with design tokens defined as CSS custom properties (oklch color space). All CSS files are declared as sideEffects in package.json, so bundlers will preserve them while still tree-shaking unused JavaScript.

CSS Import Options

Import path Use case
@tetrascience-npm/tetrascience-react-ui/index.css Pre-built CSS — for an app that owns its document. Import once at your app root.
@tetrascience-npm/tetrascience-react-ui/index.tailwind.css Tailwind source — for apps that run their own Tailwind build and want to extend/override tokens.
@tetrascience-npm/tetrascience-react-ui/index.scoped.css Scoped CSS — for code that renders inside a document it does not own (a microfrontend remote, an embedded widget). See Embedding in a host page.

Most consumers only need index.css.

What the stylesheet does — and does not — claim

Every selector rule the kit writes itself — its design tokens and its component CSS — ships inside a ts-ui-kit cascade layer, and no published stylesheet contains an unlayered rule. (Tailwind utilities land in Tailwind's utilities layer; @font-face, @keyframes and @property are not selector-matched and stay top-level, with kit keyframes ts--prefixed.) That has two consequences worth knowing:

  • Your CSS always wins. An unlayered rule outranks a layered one regardless of specificity or load order, so a plain :root { --primary: … } in your app overrides the kit's token whether it is written before or after the import. There is no ordering dance.
  • The kit never restyles your page by accident. Its component class names are namespaced (.histogram-legend-divider, .platemap-legend__item, …), and its tokens and Tailwind's preflight reset are layered beneath anything you write. A CI gate (yarn check:css-leaks) fails the build if any published stylesheet regains an unlayered rule.

Theming

The design system is controlled via CSS custom properties. Override them anywhere in your own CSS to customise colours, spacing, and radii — because the kit's tokens are layered, your unlayered declaration wins in any order:

:root {
  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0 0);
  --radius: 0.625rem;
}

Dark mode is supported via the .dark class on a parent element. See THEMING.md for details.

Embedding in a host page

index.css is written for an app that owns its document: tokens on :root / .dark, Tailwind's preflight on html / body / *. Inside someone else's document — a Module Federation remote mounted into the TetraScience platform shell, a widget dropped into a legacy page — those are not the kit's claims to make, even layered: a host that never declared --surface-bright would still pick the kit's value up document-wide.

index.scoped.css is the same stylesheet with every rule in the ts-ui-kit and base layers — tokens, component CSS, preflight — confined to an element carrying data-ts-ui-root. Import it instead of index.css and mark your shell:

import "@tetrascience-npm/tetrascience-react-ui/index.scoped.css";

export function App() {
  return (
    <div data-ts-ui-root className="dark">
      {/* kit components render with their own tokens and reset in here… */}
    </div>
  );
}

Two things to know when you use it:

  • Portaled surfaces need the marker too. Dialog, Popover, Select, Tooltip and the other overlay components render their content into document.body, outside your shell. Put data-ts-ui-root on their *Content element as well (a thin wrapper component around each one you use is the usual pattern), or they render with the host's tokens instead of the kit's.
  • Dark mode still keys off .dark. .dark on <html> (or anything above the marker), on the marked element itself, or on a dark panel nested inside a light shell all work — the scoped rules are emitted as .dark [data-ts-ui-root], [data-ts-ui-root].dark and [data-ts-ui-root] .dark.

Tailwind's own theme, properties, components and utilities layers are left global in the scoped build: they are keyed on Tailwind class names, your unlayered CSS already outranks them, and confining theme would strip --spacing / --radius-* from portaled content.

Components

UI Primitives (ui/)

shadcn/ui components built on Radix UI with Tailwind CSS and CVA variants:

Accordion, Alert, AlertDialog, AspectRatio, Avatar, Badge, Breadcrumb, Button, ButtonGroup, Calendar, Card, Carousel, Checkbox, CodeEditor, Collapsible, ComboBox, Command, ContextMenu, Dialog, DropdownMenu, Field, HoverCard, Input, InputGroup, Item, KBD, Label, MenuBar, NavigationMenu, RadioGroup, ResizablePanel, ScrollArea, Select, Separator, Sheet, Sidebar, Skeleton, Slider, Sonner, Spinner, Switch, Table, Tabs, Textarea, TetraScience Icon, Toggle, ToggleGroup, Tooltip

Composed Components (composed/)

TetraScience-specific compositions built from UI primitives:

AssistantLayout, Chat, ConfirmDialog, DataAppShell (with PrimaryNav, SecondaryNav, RightPanel), EmptyState, FormPatterns, MoleculeStructure, PlateMapEditor, ProcessFlow, RichListItem, StatCard, TdpLink, TdpSearch, TdpUrl, TopBar, UserMenu

MoleculeStructure requires a served RDKit WASM

Installing @rdkit/rdkit is not sufficient. RDKit is a ~6.6 MB WebAssembly module that the package does not place anywhere your app serves it, so the loader's fetch for RDKit_minimal.wasm falls through to your dev server's SPA fallback and gets index.html back. The component then renders its errorContent — by default "Invalid structure" — for a perfectly valid SMILES.

Point the loader at a served copy once, at app startup:

import { configureRDKit } from "@tetrascience-npm/tetrascience-react-ui";

// Option A — let your bundler emit and fingerprint it (Vite):
import wasmSrc from "@rdkit/rdkit/dist/RDKit_minimal.wasm?url";
configureRDKit({ wasmSrc });

// Option B — copy node_modules/@rdkit/rdkit/dist/RDKit_minimal.wasm into public/
configureRDKit({ wasmSrc: "/RDKit_minimal.wasm" });

To confirm it worked, the request for RDKit_minimal.wasm should return Content-Type: application/wasm at ~6.9 MB — not text/html at a few hundred bytes.

errorContent currently covers both an invalid SMILES and a failed RDKit load, so a molecule you trust showing as invalid almost always means the WASM isn't being served. Splitting the two messages is tracked in SW-2472.

ProcessFlow

Use ProcessFlow to render parent-owned multi-step workflow state such as uploads, validation pipelines, review flows, processing stages, and setup sequences. Import it from the package and keep all workflow transitions and side effects in the consuming app.

import {
  PROCESS_FLOW_STEP_STATUSES,
  ProcessFlow,
  type ProcessFlowStep,
  type ProcessFlowStepStatus,
} from "@tetrascience-npm/tetrascience-react-ui";

const steps: ProcessFlowStep[] = [
  { id: "upload", label: "Upload", description: "Choose source files", status: "completed" },
  { id: "validate", label: "Validate", description: "Check schema and lineage", status: "active" },
  { id: "publish", label: "Publish", description: "Send downstream", status: "pending" },
];

function WorkflowProgress() {
  return (
    <ProcessFlow
      steps={steps}
      selectedStepId="validate"
      onStepSelect={(step, details) => {
        console.log(step.id, details.status);
      }}
    />
  );
}

const allStatuses: readonly ProcessFlowStepStatus[] = PROCESS_FLOW_STEP_STATUSES;

Expected contract:

  • status is independently controlled per step: pending, active, completed, error, or disabled.
  • selectedStepId means the step the user is viewing or has clicked; it is separate from the active workflow state.
  • onStepSelect emits user selection only. It does not mean a workflow step completed.
  • Parent workflow code owns completion, error handling, retries, analytics, and other side effects.
  • description is shown by default. Pass showDescriptions={false} to hide all descriptions.
  • Descriptions auto-hide at narrow container widths (≤40rem) for mobile layouts.
  • The component fills 100% of its container width — size it by controlling the container.
  • Selected completed steps render with a green label; selected active steps render with a blue label.
  • Use connections and per-step position only for simple branching/configurable flows.

For AI-assisted consuming apps, add a short instruction like this to the app's AGENTS.md or CLAUDE.md:

Use `ProcessFlow` from `@tetrascience-npm/tetrascience-react-ui` for multi-step workflow visualization. Do not build a custom stepper for upload, validation, review, approval, processing, or setup flows. Parent components own the workflow state and pass `steps: ProcessFlowStep[]`; each step status must be one of `PROCESS_FLOW_STEP_STATUSES`. Use `selectedStepId` only for the viewed/selected step. Keep completion/error side effects in the parent workflow code, not inside `ProcessFlow`.

PlateMapEditor

PlateMapEditor is the standard plate-map editing surface — a metadata form, an interactive plate grid, and a sample manifest, wired together by a staged-edit controller. Use it as-is for the default layout; tune it with props; and drop to usePlateMapEditorState only when you need a layout the props can't express.

import { PlateMapEditor } from "@tetrascience-npm/tetrascience-react-ui";

function PlateScreen() {
  const [values, setValues] = React.useState(new Map());
  const [selection, setSelection] = React.useState(new Set());

  return (
    <PlateMapEditor
      format="96"
      values={values}
      onChange={setValues}
      selection={selection}
      onSelectionChange={setSelection}
      fields={FIELDS}
      tableColumns={COLUMNS}
    />
  );
}
Choosing a level
You need Use
The standard surface, tuned by props PlateMapEditor
The form somewhere the editor can't reach PlateMapEditor + hideForm + the imperative handle
A layout no prop combination expresses usePlateMapEditorState + PlateMapForm / PlateMapGrid / PlateMapManifest

Dropping to the hook keeps the apply/clear semantics, plate scoping, and barcode stamping — you only take over layout. Do not re-implement staged edits by hand.

Customization
  • Layout — formPlacement (start / end / top / bottom), stackAt, formWidth, hideForm, hideManifest.
  • Slots — banner (whole editor), plateBanner (plate card only), plateToolbar (above grid), plateFooter (the plate card's footer), footer (editor-wide action row), legend + legendPlacement, formExtras, formSlot, manifestSlot.
  • Edit state — staged / onStagedChange for a controlled staged record, mergeOnApply for custom merge semantics, applyScope="all-plates" to write across every plate at once.
  • Manifest — one manifest object: { filterable, filterColumns, groupable, defaultGroupBy, pageSize, pageSizeOptions, enableFillDown }. Structural bits stay top-level: hideManifest, manifestTitle, manifestSlot.
  • Labels — one labels object covers every string the editor and its manifest render, typed as the exported PlateMapEditorLabels, so an app's translation table is a type error away from going stale: const fr: PlateMapEditorLabels = { … }. plateTitle / manifestTitle and the import/export menu labels are separate props (they take ReactNode, not plain text).
  • Styling — className / style on the root, plus one classNames map for the regions (PlateMapEditorClassNames): { layout, formCard, plateCard, manifestCard, form, grid, manifest }. Each card also carries data-plate-map-region="form|plate|manifest", so plain CSS can target the same regions without threading props.
Rendering the form outside the editor

Hide the built-in form and drive the staged state through the imperative handle. The editor keeps owning selection and apply; your form just feeds it.

const editor = React.useRef<PlateMapEditorHandle<MyWell>>(null);

<>
  <MySidebarForm
    onChange={(next) => editor.current?.setStaged(next)}
    onApply={() => editor.current?.apply()}
    onClear={() => editor.current?.clear()}
  />
  <PlateMapEditor ref={editor} hideForm {...rest} />
</>;
Responsiveness

The editor responds to its container's width, not the viewport's, so it lays out correctly inside a narrow panel, split pane, or drawer on a wide screen. stackAt names a container width — sm 640 / md 768 (default) / lg 1024 / xl 1280 / never — below which the form and grid stack full-width. A plate too dense to fit scrolls inside its own container rather than widening the page.

Migration notes

Everything below is additive; existing code keeps working unchanged.

  • Layout now tracks container width, not viewport width. This is the one behavioural change. If your editor is full-width the result is effectively the same; if it sits in a narrow panel on a wide screen it will now correctly stack instead of rendering a cramped two-column layout. Tune with stackAt.
  • colorForWell and emptyEntry are now optional — delete them for read-only or single-category views and sensible defaults apply.
  • className on the root always worked despite reports otherwise; style is new.
  • If you previously forked or re-assembled the primitives to change layout, replace that with formPlacement / stackAt / formWidth and the slots, or with usePlateMapEditorState if you still need custom structure. Hand-rolled staged/apply logic should be deleted in favour of the hook.
  • If you worked around the hardcoded 360px form column with a width utility, switch to formWidth — it is the supported path and applies only at and above stackAt.
  • manifestFilterable and manifestGroupable still work but are deprecated in favour of manifest={{ filterable, groupable }}.

Charts (charts/)

Plotly.js-based data visualisations:

AreaPlot, BarChart, BoxPlot, Chromatogram, StackedChromatogram, Electropherogram, Histogram, LinePlot, PieChart, PlateMap, ScatterPlot, ScatterPlotInteractive

Server Utilities

Beyond UI components, this library includes server-side helper functions for building TetraScience applications. These are available via the /server subpath to avoid pulling Node.js dependencies into browser bundles.

Authentication (server/auth)

JWT Token Manager - Manages JWT token retrieval for data apps:

import { jwtManager } from "@tetrascience-npm/tetrascience-react-ui/server";

// In Express middleware
app.use(async (req, res, next) => {
  const token = await jwtManager.getTokenFromExpressRequest(req);
  req.tdpAuth = { token, orgSlug: process.env.ORG_SLUG };
  next();
});

// Or with raw cookies
const token = await jwtManager.getUserToken(req.cookies);

Environment Variables:

  • ORG_SLUG - Organization slug (required)
  • CONNECTOR_ID - Connector ID for ts-token-ref flow
  • TDP_ENDPOINT - API base URL
  • TS_AUTH_TOKEN - Service account token (fallback for local dev)

Note: The singleton jwtManager reads environment variables when the module is imported. Ensure these are set before importing the module.

Data App Providers (server/providers)

TypeScript equivalents of the Python helpers from ts-lib-ui-kit-streamlit for connecting to database providers (Snowflake, Databricks, Athena).

Getting Provider Configurations:

import { TDPClient } from "@tetrascience-npm/ts-connectors-sdk";
import { getProviderConfigurations, buildProvider, jwtManager } from "@tetrascience-npm/tetrascience-react-ui/server";

// Get user's auth token from request (e.g., in Express middleware)
const userToken = await jwtManager.getTokenFromExpressRequest(req);

// Create TDPClient with the user's auth token
// Other fields (tdpEndpoint, connectorId, orgSlug) are read from environment variables
const client = new TDPClient({
  authToken: userToken,
  artifactType: "data-app",
});
await client.init();

// Get all configured providers for this data app
const providers = await getProviderConfigurations(client);

for (const config of providers) {
  console.log(`Provider: ${config.name} (${config.type})`);

  // Build a database connection from the config
  const provider = await buildProvider(config);
  const results = await provider.query("SELECT * FROM my_table LIMIT 10");
  await provider.close();
}

Using Specific Providers:

import {
  buildSnowflakeProvider,
  buildDatabricksProvider,
  getTdpAthenaProvider,
  type ProviderConfiguration,
} from "@tetrascience-npm/tetrascience-react-ui/server";

// Snowflake
const snowflakeProvider = await buildSnowflakeProvider(config);
const data = await snowflakeProvider.query("SELECT * FROM users");
await snowflakeProvider.close();

// Databricks
const databricksProvider = await buildDatabricksProvider(config);
const data = await databricksProvider.query("SELECT * FROM events");
await databricksProvider.close();

// TDP Athena (uses environment configuration)
const athenaProvider = await getTdpAthenaProvider();
const data = await athenaProvider.query("SELECT * FROM files");
await athenaProvider.close();

Exception Handling:

import {
  QueryError,
  MissingTableError,
  ProviderConnectionError,
  InvalidProviderConfigurationError,
} from "@tetrascience-npm/tetrascience-react-ui/server";

try {
  const results = await provider.query("SELECT * FROM missing_table");
} catch (error) {
  if (error instanceof MissingTableError) {
    console.error("Table not found:", error.message);
  } else if (error instanceof QueryError) {
    console.error("Query failed:", error.message);
  }
}

Environment Variables:

  • DATA_APP_PROVIDER_CONFIG - JSON override for local development only
  • CONNECTOR_ID - Connector ID for fetching providers from TDP
  • TDP_ENDPOINT - TDP API base URL
  • ORG_SLUG - Organization slug
  • ATHENA_S3_OUTPUT_LOCATION - S3 bucket for Athena query results
  • AWS_REGION - AWS region for Athena

Note: Authentication tokens are obtained from the user's JWT via jwtManager. The TS_AUTH_TOKEN environment variable is only for local development fallback.

Connector Key/Value Store

The TDP connector key/value store lets data apps persist small pieces of state (user preferences, cached results, last-run timestamps, etc.) without an external database. The TDPClient from @tetrascience-npm/ts-connectors-sdk provides getValue, getValues, saveValue, and saveValues methods.

Reading and writing values with the user's JWT token:

import { TDPClient } from "@tetrascience-npm/ts-connectors-sdk";
import { jwtManager } from "@tetrascience-npm/tetrascience-react-ui/server";

// In an Express route handler:
app.get("/api/kv/:key", async (req, res) => {
  // 1. Get the user's JWT from request cookies
  const userToken = await jwtManager.getTokenFromExpressRequest(req);
  if (!userToken) return res.status(401).json({ error: "Not authenticated" });

  // 2. Create a TDPClient authenticated as the user
  //    (CONNECTOR_ID, TDP_ENDPOINT, ORG_SLUG are read from env vars)
  const client = new TDPClient({
    authToken: userToken,
    artifactType: "data-app",
  });
  await client.init();

  // 3. Read a value
  const value = await client.getValue(req.params.key);
  res.json({ key: req.params.key, value });
});

app.put("/api/kv/:key", async (req, res) => {
  const userToken = await jwtManager.getTokenFromExpressRequest(req);
  if (!userToken) return res.status(401).json({ error: "Not authenticated" });

  const client = new TDPClient({
    authToken: userToken,
    artifactType: "data-app",
  });
  await client.init();

  // Write a value (any JSON-serialisable type)
  await client.saveValue(req.params.key, req.body.value, { secure: false });
  res.json({ key: req.params.key, saved: true });
});

Reading multiple values at once:

const values = await client.getValues(["theme", "locale", "last-run"]);
// values[0] → theme, values[1] → locale, values[2] → last-run

See the example app for a complete working server with KV store endpoints.

TDP Search (server)

TdpSearchManager - Server-side handler for the TdpSearch component. Resolves auth from request cookies (via jwtManager), calls TDP searchEql, and returns the response so the frontend hook works with minimal wiring.

import { tdpSearchManager } from "@tetrascience-npm/tetrascience-react-ui/server";

// Express: mount a POST route (e.g. /api/search)
app.post("/api/search", express.json(), async (req, res) => {
  try {
    const body = req.body; // SearchEqlRequest (searchTerm, from, size, sort, order, ...)
    const response = await tdpSearchManager.handleSearchRequest(req, body);
    res.json(response);
  } catch (err) {
    res.status(401).json({ error: err instanceof Error ? err.message : "Search failed" });
  }
});

Frontend: use <TdpSearch columns={...} /> with default apiEndpoint="/api/search", or pass apiEndpoint if you use a different path. Auth is taken from cookies (ts-auth-token or ts-token-ref via jwtManager).

TypeScript Support

Full TypeScript support with exported types:

import { Button } from "@tetrascience-npm/tetrascience-react-ui";
import type { ButtonProps, BarChartProps, BarDataSeries } from "@tetrascience-npm/tetrascience-react-ui";

Testing your app with Jest

The kit ships dual ESM + CJS output, so Jest's CommonJS runtime can load every component directly — no need to mock the package. What Jest can't load are a few third-party dependencies that publish ESM-only (the streamdown/markdown stack, shiki, use-stick-to-bottom, react-resizable-panels) and optional peers you may not have installed (plotly.js-dist, @rdkit/rdkit). The kit ships a single setup file that stubs exactly those, plus the jsdom shims Radix-based components need (ResizeObserver, matchMedia, pointer capture, …).

Add one line to jest.config.js:

module.exports = {
  testEnvironment: "jsdom",
  setupFiles: ["@tetrascience-npm/tetrascience-react-ui/jest-setup"],
};

Requires Jest ≥ 28 (package exports support) and jest-environment-jsdom. To override any stub, register your own mock — jest.mock("<module>", …) in a test file or a later setup file replaces the kit's registration. If Jest runs with injectGlobals: false, import installUiKitJestMocks / installUiKitDomShims from the same module and call them from your own setup file with the jest object.

What the stubs do:

  • Charts render their containers; Plotly calls resolve against an inert stub (jsdom has no WebGL). Assert on props/behavior, not pixels — visual assertions belong in a real browser.
  • MessageResponse / Reasoning render the markdown source as plain text, so text-content assertions work without transpiling the markdown ecosystem.
  • CodeBlock renders unhighlighted code lines. Only the languages the kit ships by default are covered — a grammar you add yourself via registerCodeBlockLanguage isn't mockable by this setup file, since it isn't known ahead of time.
  • MoleculeStructure resolves against a stub that always returns a valid, empty-SVG molecule. For real assertions (invalid-SMILES handling, actual rendered markup), use the kit's own override hook instead of relying on the stub: configureRDKit({ importFactory: () => Promise.resolve(myFakeRDKitModule) }), exported alongside MoleculeStructure.

Examples

This repository uses component driven development with Storybook. To see the examples run the following.

# Clone the repository
git clone https://github.com/tetrascience/ts-lib-ui-kit.git
cd ts-lib-ui-kit

# Install dependencies
yarn

# Run the storybook
yarn dev

Visit http://localhost:6006.

Documentation

MCP server (for AI coding agents)

This library exposes an MCP server so AI coding agents (Claude Code, Cursor, Claude Desktop) can query authoritative component lists, prop/variant options, and usage examples instead of guessing — reducing hallucinated component APIs when scaffolding a data app.

There are two endpoints. Pick whichever fits; you can add both.

Endpoint URL Tools
Deployed (no local checkout needed) https://ts-lib-ui-kit-storybook.vercel.app/api/mcp docs: list_components, get_component, search_components
Local (needs yarn storybook running) http://localhost:6006/mcp full set: docs + write/preview/test stories

Add the connection

Claude Code — register the deployed server (HTTP transport):

claude mcp add --transport http ts-ui-kit https://ts-lib-ui-kit-storybook.vercel.app/api/mcp

Use --scope project to share it with your team via a checked-in .mcp.json, or --scope user to make it available across all your projects. For the local server, run yarn storybook first, then:

claude mcp add --transport http ts-ui-kit-local http://localhost:6006/mcp

Cursor / Claude Desktop / other clients — add an HTTP MCP server to the client's MCP config (e.g. Cursor's .cursor/mcp.json, or Claude Desktop's claude_desktop_config.json):

{
  "mcpServers": {
    "ts-ui-kit": {
      "type": "http",
      "url": "https://ts-lib-ui-kit-storybook.vercel.app/api/mcp"
    }
  }
}

Any client (generic helper):

npx mcp-add --type http --url "https://ts-lib-ui-kit-storybook.vercel.app/api/mcp"

Then ask your agent something like "using the ts-ui-kit MCP, list the available components" or "build a form using ts-ui-kit primitives" to confirm it's wired up.

Tech Stack

  • React 19
  • TypeScript
  • Tailwind CSS 4
  • shadcn/ui (Radix UI)
  • Vite 7
  • Plotly.js (charts)
  • Monaco Editor (code editing)

License

Licensed under the Apache License, Version 2.0 – see LICENSE for details.

Releases

Used by

Contributors

Languages