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

@tetralabs/loom-react-ui

v0.1.0

Published

React chat controllers and dashboard components for Loom

Readme

@tetralabs/loom-react-ui

Pre-release: This package is under active development, and its API may change before the first stable release.

React composition primitives for Loom chat and dashboards.

The package has two independent entry points:

  • /chat: Chat controllers, composable views, and a styled default renderer.
  • /dashboard: Dashboard controllers, binding utilities, layouts, chart and content widgets, and a complete renderer.

Consumers own routing, notifications, dialogs, and application-specific widgets.

Start here

| Goal | Use | Import | | --------------------------------------- | ------------------------------------------- | ----------------------------------- | | Complete chat with default UI | LoomChatRoot | @tetralabs/loom-react-ui/chat | | Replace transcript or composer UI | Focused component render props | @tetralabs/loom-react-ui/chat | | Build a fully custom chat UI | useLoomConversation inside LoomChatRoot | @tetralabs/loom-react-ui/chat | | List or manage chat sessions | Session, sharing, and retrieval hooks | @tetralabs/loom-react-ui/chat | | Render a saved dashboard | LoomDashboard | @tetralabs/loom-react-ui/dashboard | | Insert UI between loading and rendering | LoomDashboardRoot + LoomDashboardBoard | @tetralabs/loom-react-ui/dashboard | | Build a custom dashboard layout | LoomDashboardView or useLoomDashboard | @tetralabs/loom-react-ui/dashboard | | Render prepared descriptors | Screenboard or Timeboard | @tetralabs/loom-react-ui/dashboard |

The package intentionally has no top-level import. Import from /chat or /dashboard so applications pay only for the surface they use.

Package boundaries

  • @tetralabs/loom owns API types, the runtime-neutral client, serialized dashboard definitions, and errors.
  • @tetralabs/loom-react owns query hooks and low-level React controllers.
  • @tetralabs/loom-react-ui owns presentation-ready controllers, composition boundaries, layouts, and default widgets.
  • The consuming application owns routing, persistence workflows, notifications, dialogs, authorization decisions, and product-specific UI.

Installation

Install the package with the peers needed by the entry points you consume.

npm install @tetralabs/loom @tetralabs/loom-react @tetralabs/loom-react-ui @tanstack/react-query react react-dom

The package is ESM-only and supports React 18 or newer and TanStack Query v5.

For the dashboard entry, also install its widget and layout peers:

npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities @tanstack/markdown lucide-react recharts

Setup

Create one Loom client and render both required providers above components that use this package. getHeaders runs before every request, so it can refresh an expiring access token.

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createLoomClient } from "@tetralabs/loom";
import { ClientProvider } from "@tetralabs/loom-react";
import type { ReactNode } from "react";

// Create clients once so they remain stable across renders.
const queryClient = new QueryClient();
const loomClient = createLoomClient({
  baseUrl: "https://loom.example.com",
  // Resolve headers per request to support refreshed access tokens.
  getHeaders: async () => ({
    Authorization: `Bearer ${await getAccessToken()}`,
  }),
});

export function AppProviders({ children }: { children: ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      <ClientProvider client={loomClient}>{children}</ClientProvider>
    </QueryClientProvider>
  );
}

Chat

LoomChatRoot creates the underlying chat controller. It accepts the same options as ChatProvider from @tetralabs/loom-react/chat. Import the optional stylesheet and render LoomChatRoot without children for the complete default interface.

import { LoomChatRoot } from "@tetralabs/loom-react-ui/chat";
import "@tetralabs/loom-react-ui/chat.css";

export function Chat() {
  return (
    <LoomChatRoot
      options={{
        // Let the controller create and retain the active session ID.
        session: { mode: "uncontrolled", initialId: null },
        capabilities: {
          activity: "replay",
          approvals: { autoResume: true },
          cancellation: true,
        },
        onError: reportError,
      }}
    />
  );
}

The stylesheet uses package-prefixed selectors and CSS custom properties. It is optional: omit it when supplying all presentation through render props.

Separate messages and composer

LoomChatMessages and LoomChatComposer consume the same root controller and can be placed or replaced independently. With no children, each renders its built-in interface.

import {
  LoomChatComposer,
  LoomChatMessages,
  LoomChatRoot,
} from "@tetralabs/loom-react-ui/chat";

<LoomChatRoot options={options}>
  <main>
    <LoomChatMessages />
  </main>
  <footer>
    <LoomChatComposer placeholder="Ask about this dashboard..." />
  </footer>
</LoomChatRoot>;

Composer slots

Use leading and toolbar to augment the built-in composer. Slots accept a node or a function receiving the focused composer controller. actions replaces the default Send and Stop controls; children still replaces the entire component.

<LoomChatComposer
  leading={<SelectedSources />}
  toolbar={(composer) => <span>{composer.value.length} characters</span>}
  actions={(composer) => (
    <MySendButton
      disabled={!composer.canSubmit}
      onClick={() => void composer.submit()}
    />
  )}
/>

Both components accept an optional focused render prop. This allows one half to remain built in while the other uses application-specific UI.

<LoomChatRoot options={options}>
  <LoomChatMessages>
    {(messages) => <MessageTimeline items={messages.items} />}
  </LoomChatMessages>
  <LoomChatComposer />
</LoomChatRoot>

Advanced composition

Use useLoomConversation when one component needs the complete conversation controller rather than one of the focused render props.

import {
  LoomChatRoot,
  useLoomConversation,
} from "@tetralabs/loom-react-ui/chat";

function CustomConversation() {
  const chat = useLoomConversation();
  return (
    <CustomChat
      items={chat.items}
      status={chat.status}
      error={chat.error}
      onSend={chat.send}
      onStop={chat.cancel}
    />
  );
}

<LoomChatRoot options={options}>
  <CustomConversation />
</LoomChatRoot>;

Conversation status is one of welcome, loading, error, empty, or ready.

Use a controlled session when routing owns the active conversation:

<LoomChatRoot
  options={{
    session: {
      mode: "controlled",
      id: sessionId,
      onChange: (nextId) => navigateToSession(nextId),
    },
  }}
>
  {children}
</LoomChatRoot>

See the @tetralabs/loom-react documentation for the complete chat controller contract.

Chat hooks

All hooks require the QueryClientProvider and ClientProvider shown in Setup. Only useLoomConversation additionally requires LoomChatRoot.

| API | Use it for | Important contract | | ----------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | useLoomConversation() | A custom component owning the complete chat UI | Combines messages, activity, approvals, actions, and presentation status. | | useLoomChatSessionsController(options) | Active/archived session lists | status is local query state; filter runs client-side; activeSessionId marks but does not select a root session. | | useLoomChatSessionActions(sessionId) | Rename, pin, archive, or update | All actions share one mutation's pending/error state; reset() clears that error. | | useLoomChatSharing(sessionId, enabled) | Load and replace sharing policy | Load and save errors are separate. save() rejects without a session ID. | | useLoomRetrievalScope(options) | Build source/entity restrictions | Source loading is opt-in. Empty selection produces undefined, preserving server defaults. | | buildLoomRetrievalScope(entityTypes, sources) | Convert selections without React state | Deduplicates selected source entity IDs. |

import { useLoomChatSessionsController } from "@tetralabs/loom-react-ui/chat";

function SessionList({ activeId }: { activeId?: string }) {
  const sessions = useLoomChatSessionsController({ activeSessionId: activeId });
  if (sessions.isPending) return <p>Loading...</p>;
  if (sessions.error) {
    return <button onClick={() => void sessions.retry()}>Retry</button>;
  }
  return sessions.items.map(({ session, isActive }) => (
    <button key={session.id} aria-current={isActive || undefined}>
      {session.title ?? "Untitled"}
    </button>
  ));
}

Dashboards

LoomDashboard loads a saved definition, runs its enabled queries, and renders the configured screenboard or timeboard. Registries are supplied once.

import {
  LoomDashboard,
  type LoomDashboardRegistries,
} from "@tetralabs/loom-react-ui/dashboard";

const registries: LoomDashboardRegistries = {
  valueFormatters: {
    currency: (value) => `$${Number(value).toFixed(2)}`,
  },
};

export function Dashboard({ dashboardKey }: { dashboardKey: string }) {
  return <LoomDashboard dashboardKey={dashboardKey} registries={registries} />;
}

Set enabled={false} to suppress both definition and widget-query requests.

Choose a dashboard layer

| Requirement | Preferred API | | -------------------------------------------- | ------------------------------------------ | | Render a saved definition unchanged | LoomDashboard | | Add toolbar, status, or surrounding content | LoomDashboardRoot + LoomDashboardBoard | | Replace one serialized widget type | registries.widgetRenderers | | Resolve data normally but replace the layout | LoomDashboardView or useLoomDashboard | | Render local widget descriptors | Screenboard or Timeboard | | Resolve bindings outside the controller | resolveLoomDashboardBindings |

Screenboard or Timeboard

| Layout | Choose it when | Editing contract | | ------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | Screenboard | Uniform widgets where order matters more than exact placement | editMode enables drag reorder and calls onReorder. | | Timeboard | Mixed-size widgets needing explicit placement | Uses one-based coordinates. editMode enables drag/resize, honors minimum spans, resolves collisions downward, and calls onChange. |

Both layouts expose data-widget-id and data-widget-type on shells. Pass a string or descriptor callback to itemClassName and itemStyle. The package does not persist edit results; save callback output in the application.

Custom widgets and overrides

Register a renderer under the serialized widget type. The same mechanism adds application-specific widget types and replaces built-in widgets such as stat or bar. Renderers receive the widget config, resolved props, query state, and retry action.

import {
  LoomDashboard,
  type LoomDashboardWidgetRendererProps,
} from "@tetralabs/loom-react-ui/dashboard";

function SalesPipeline({ props, isPending }: LoomDashboardWidgetRendererProps) {
  if (isPending) return <p>Loading...</p>;
  return <SalesChart data={props.data} />;
}

function BrandedStat({ props }: LoomDashboardWidgetRendererProps) {
  return <MetricCard label={props.label} value={props.value} />;
}

<LoomDashboard
  dashboardKey="sales"
  registries={{
    widgetRenderers: {
      salesPipeline: SalesPipeline,
      stat: BrandedStat,
    },
  }}
/>;

An exact widgetRenderers match takes precedence over a built-in renderer. The existing type: "custom" plus componentKey and customWidgets registry remain supported for compatibility.

Dashboard styling

Use board props to style the board and widget shells. Every shell also exposes data-widget-id and data-widget-type attributes for stable CSS selectors.

<LoomDashboard
  dashboardKey="sales"
  timeboardProps={{
    className: "sales-dashboard",
    itemClassName: (widget) =>
      `widget-shell widget-${widget.widgetType ?? widget.type}`,
    itemStyle: (widget) =>
      widget.id === "revenue" ? { border: "2px solid green" } : undefined,
  }}
/>

Use a renderer override when styling requires changing a widget's internal markup. Shell hooks are intended for spacing, borders, backgrounds, and layout integration.

Dashboard bindings

Widget props can contain declarative bindings that are resolved before the renderer receives them:

  • { $query: "total", default: 0 } returns one query result.
  • { $sum: ["open", "closed"] } sums numeric query results.
  • { $reindex: { source: "counts", order: ["A", "B"] } } orders categorical points.
  • { $table: { source: "rows", rowFormatter: "contact" } } optionally maps table rows.
  • { $template: "{{count}} total", vars: { count: { $query: "total" } } } interpolates values.
  • { $format: { source: "revenue", formatter: "currency" } } applies a registered formatter.

Use resolveLoomDashboardBindings for custom composition and collectLoomDashboardQueryKeys when a renderer needs to inspect a binding's query dependencies. Formatter names come from the registries passed to LoomDashboardRoot or directly to the resolver.

Registry reference

| Registry | Key | Value | Use | | ----------------- | ------------------------- | -------------------------------------------- | --------------------------------------------------------------------- | | widgetRenderers | Serialized widget type | LoomDashboardWidgetRendererProps component | Add a type or replace a built-in renderer. Preferred extension point. | | customWidgets | Serialized componentKey | Component receiving resolved props | Compatibility path for type: "custom". | | icons | Serialized icon name | Component receiving size | Resolve a widget definition's named icon. | | valueFormatters | Formatter name | (value) => unknown | Resolve $format and string-valued valueFormatter props. | | rowFormatters | Formatter name | (row) => unknown | Transform rows referenced by $table. |

Built-in widget catalog

| Type | Component | Primary input | Notable behavior | | --------- | ------------------ | --------------------------------------------- | ------------------------------------------------------------------------------------ | | area | TimeseriesWidget | Records with label and declared series keys | Line/area/bar series, dual axes, markers, events, legends, point links, peak labels. | | bar | BarWidget | { name, ...numericSeries }[] | Vertical/horizontal, flat/stacked/relative modes, labels, drill-down click. | | change | ChangeWidget | { label, current, previous }[] | Delta bars, invertible direction, optional interactive sorting. | | donut | DonutWidget | { name, value }[] plus optional rings | Focused sectors, total, legend modes, links, fullscreen. | | funnel | FunnelWidget | Ordered { name, value }[] stages | Step and end-to-end conversion visualization. | | gauge | GaugeWidget | Percentage value from 0 to 100 | Default threshold color, optional pulse and stats. | | iframe | IframeWidget | External src | Lazy sandboxed iframe with restrictive defaults. | | image | ImageWidget | Image or video src | Fit, alignment, background, optional navigation URL. | | note | NoteWidget | Markdown content | Variants, alignment, $name.value interpolation, decorative tick. | | scatter | ScatterWidget | { x, y, z?, name? }[] or named series | Bubble size, drag zoom, outlier labels, density contours, point click. | | stat | QueryValue | One string or numeric value | Precision/scaling, trend, thresholds, icon, sparkline. | | table | TableWidget | Row records and column definitions | Search, sorting, limits, bars, aliases, conditional formatting, row click. | | text | TextWidget | Plain text | Size, alignment, foreground and named background colors. | | toplist | TopListWidget | Ranked { label, value }[] | Flat/stacked bars, scaling, sorting, conditional colors, links. | | treemap | TreemapWidget | Flat leaves or one level of groups | Proportional cells, drill-down, links, fullscreen. |

Widgets accept presentation-ready data and do not issue domain queries. Most framed widgets accept isLoading and error; LoomDashboardBoard supplies those from widget query state unless resolved props already define them.

Related packages

License

MIT. See LICENSE.