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

@tangle-network/ui

v11.51.12

Published

Generic React UI components for Tangle products — primitives, chat, run, files, editor, markdown.

Readme

@tangle-network/ui

Minimal graph and server hosts can import the existing clock from @tangle-network/ui/hooks/use-live-time and status metadata/glyphs from @tangle-network/ui/run/status-meta. These module entries share the root bindings without loading optional session stores.

Generic React UI components for Tangle products. Imports tokens from @tangle-network/brand (peer dep). Tailwind v4-native.

Component visual precedent is governed by the repo-level design bible and component audit. Storybook examples are test surfaces; they are not automatically approved brand patterns.

Install

pnpm add @tangle-network/ui @tangle-network/brand react react-dom

Use

import { Button } from "@tangle-network/ui/primitives";
import { Logo } from "@tangle-network/brand";

export function App() {
  return (
    <>
      <Logo size="md" />
      <Button>Click</Button>
    </>
  );
}

partitionBySpeaker and the speaker types in /chat re-export the canonical @tangle-network/agent-interface/speaker contract. Server consumers import that entry directly; SpeakerHeader remains the UI renderer.

Styling

This package ships no CSS. Its components are Tailwind v4 class names in source, and the build emits JavaScript only, so two pieces of consumer setup carry real weight.

Scan this package

Your Tailwind or UnoCSS build has to read this package's files. Neither scans node_modules on its own, so a class that only this package names is never generated. Nothing fails loudly when one is missing: the element still renders, still occupies space and still takes clicks, it just looks inert. A Switch with no track colour in either state, a Tabs where the selected tab is indistinguishable from the rest, and a Textarea collapsed to one row all come out of this.

Tailwind v4, in the stylesheet that imports tailwindcss. @source paths resolve relative to that stylesheet, so adjust the prefix to wherever yours lives:

/* from src/styles/app.css, with node_modules two levels up */
@source "../../node_modules/@tangle-network/ui/src/**/*.tsx";
@source "../../node_modules/@tangle-network/ui/src/**/*.ts";

sandbox-ui runs exactly these two globs in src/styles/globals.css, alongside @import "tailwindcss" source(none) so the scan is the whole of what the bundle compiles from.

UnoCSS, in uno.config.ts:

content: {
  filesystem: ["node_modules/@tangle-network/ui/dist/**/*.js"],
}

Under pnpm that plain glob can match zero files. node_modules/@tangle-network/ui is a symlink into node_modules/.pnpm, and UnoCSS's globber does not follow it, which leaves you exactly where you started with no error. Pre-expand the glob with something that does follow symlinks, resolve each hit through realpath, and hand UnoCSS concrete paths. tangle-network/blueprint-agent's apps/web/uno.config.ts carries a worked version that walks the import graph of the two entries it uses rather than the whole dist.

Two things to check after wiring this up, because the failure is silent either way. Read the generated CSS for a class this package alone names, rather than eyeballing the app. And assert the scan resolved a non-zero number of files: a scan that silently matches nothing looks identical to no scan at all.

One consequence worth knowing. This is a scan, not a manifest, so a consumer emits only the classes the version it installed actually names. Upgrading this package can introduce a class your build has never generated, and it will go missing the same silent way.

Import the brand stylesheet

@tangle-network/brand is a peer dependency. It holds the tokens every colour here resolves through, and a few real classes that some components reference by name instead of composing out of utilities. Badge with dot is the one to know about: it emits status-dot status-dot-running and its siblings, which are defined in @tangle-network/brand/styles/globals.css and nowhere else. Without that import the dots have no size and no colour.

@import "@tangle-network/brand/styles";
@import "tailwindcss";

See @tangle-network/brand's README for the token-level breakdown, the finer-grained imports, and the fonts, which brand deliberately does not bundle.

Focus treatment

Every control shares one focus treatment, exported from utils as class strings. Compose them with cn so tailwind-merge resolves any override.

  • focusField — a text field: neutral hairline, a slightly darker border on hover, and on :focus a --focus-border border with a 3px --focus-halo ring.
  • focusFieldWithin — the same, for a container that wraps a borderless field, such as a composer around a textarea. It lights while a text input, textarea, native <select> or contenteditable editor inside it has focus, not while a button inside it does. It keys off :has(), so a focus-within:* class does not override it.
  • focusFieldInvalid — the error state of a field. Compose it after focusField.
  • focusRing — keyboard focus on a button, tab, switch or link: a 1px --focus-border outline with the halo beyond it, on :focus-visible only.
  • focusRingInset — focusRing drawn inside the box, for a control in a scrolling strip.

The colors come from the --focus-* tokens in @tangle-network/brand, so they need brand 1.6 or later. To remove the ring from a field nested in a focusable container, pass focus:ring-0 border-0 and put focusFieldWithin on the container.

Subpaths

Seventeen named exports: primitives, chat, run, openui, openui-schema, files, editor, markdown, auth, hooks, sdk-hooks, stores, types, utils, tool-previews, nav, redaction.

openui-schema has no React imports. Its validation functions are compiled from the shared JSON Schema at build time, so importing the subpath does not use dynamic code generation at Worker startup. Use OPENUI_NODE_JSON_SCHEMA to describe one model-authored node and validateOpenUIJsonNode(value) before writing it. Use validateOpenUIJsonArtifact(value) for a saved single node or node array. Both validators return { ok: true, value } with a typed renderer node, or { ok: false, issue: { path, message } }. They reject unknown node types, hidden fields, malformed cells, and unsupported nested children. The renderer reports an unsupported saved node at its path instead of silently omitting it.

Markdown and OpenUI links

Markdown adds GitHub-style heading IDs before sanitising the rendered document. The existing sanitizer keeps its user-content- prefix to prevent DOM clobbering. For a link into another Markdown file, use getSanitizedMarkdownHeadingIdFromRawFragment with raw fragment text, without a leading # or sanitizer prefix. Markdown applies the same mapping to local #fragment links. Custom URL transforms still pass through ReactMarkdown's safe protocol check. OpenUIArtifactRenderer forwards its urlTransform prop to nested Markdown nodes.

Optional peers

@tiptap/core, @tiptap/react, @tiptap/starter-kit, @tiptap/extension-collaboration, @tiptap/extension-collaboration-caret, @hocuspocus/provider and yjs back the ./editor entry. The entry reaches every one of them through a dynamic import(), and gets a loud error that names the missing packages only when it renders an editor. ./editor splits the cost three ways, so each surface costs only the peers it uses:

| Surface | Peers it needs | | --- | --- | | DocumentEditorPane in preview mode | none | | MarkdownDocumentEditor (local) | @tiptap/react, @tiptap/starter-kit | | EditorProvider (collaboration transport) | @hocuspocus/provider, yjs | | TiptapEditor (collaborative) | all of the above, plus both collaboration extensions |

EditorProvider builds the document and the socket from yjs and @hocuspocus/provider alone, so a consumer that drives its own editor from its context — through useEditorConnection, useCollaborators and the other ./editor hooks — installs those two and no tiptap package.

A bundler still reads the literal specifier in a dynamic import(), so what a consumer without the peers must do depends on the bundler:

| Bundler | A consumer that installs none of the peers | | --- | --- | | Vite, Rollup | Builds. The unresolved peer becomes a chunk that throws when it loads. | | esbuild | Builds. Each import carries a .catch(), which moves the unresolved path from build time to run time. | | webpack | Needs configuration. Give resolve.fallback the value false for each peer you do not install, for example resolve: { fallback: { "@tiptap/react": false } }. |

A TypeScript consumer resolves these entries through the emitted declarations, and those declarations name the optional peers in their own import statements. Keep skipLibCheck: true — the common default — and a consumer without the peers type-checks: its own imports resolve, and TypeScript skips the declaration files that name the packages it does not have. With skipLibCheck: false, TypeScript reads those files and reports TS2307 for each absent peer; install the peers, or add a module declaration for each one you omit.

pnpm test:package builds a packed consumer that installs none of the peers, under both Vite and esbuild, and type-checks it. scripts/validate-dist.mjs rejects a static import of any of them, and rejects a dynamic import that lost its .catch().

Because the peers now resolve at first render rather than at build time, a missing one surfaces as a thrown error while React renders. Wrap the editors in an error boundary, so the install list reaches a surface you control instead of unmounting the tree.

PDF pages

pdfjs-dist backs PdfViewer in ./files, which FilePreview and FileArtifactPane use for PDFs. It follows the editor's rule: the entry reaches it only through a caught dynamic import(), so a consumer without it builds under Vite and esbuild. Without it, PdfViewer says so and shows the browser's own viewer through <object>, which Android Chrome does not have, so a reader there gets a download card instead of pages.

pnpm add pdfjs-dist

PdfViewer imports pdf.js's legacy build, which carries the polyfills that phone browsers a year or two old need. Two settings are page-wide, because pdf.js's own worker setting is. Set them once, before the first viewer renders:

import { configurePdfViewer } from "@tangle-network/ui/files";
import workerSrc from "pdfjs-dist/legacy/build/pdf.worker.min.mjs?url"; // Vite

configurePdfViewer({ workerSrc, assetsUrl: "/pdfjs/" });
  • workerSrc moves parsing into a worker. Without it, the viewer runs pdf.js's worker code on the main thread, which needs no bundler setup and no CDN, at the cost of scrolling while a large file parses.
  • assetsUrl is where the app serves the installed pdfjs-dist's wasm/, cmaps/, standard_fonts/ and iccs/ directories. pdf.js 6 decodes JBIG2, CCITT fax and JPEG 2000 images with the wasm modules, and black-and-white scans use the first two, so without it a scanned page renders blank. The viewer warns once in the console when it is missing. Copy the directories at build time; they must come from the same pdfjs-dist version as the worker.

An http(s) src on another origin needs CORS, as any fetch does. When it fails, the viewer falls back to <object>, which does not.

nanostores and @nanostores/react back ./stores, and react-router backs ./nav. Those entries create values at module scope, so consumers must install their peers before importing them. The package root also re-exports ./stores, so a root import requires both nanostores peers. Consumers that use only other subpath entries do not need these three peers.

Retained report readers can import existing chat/agent-timeline, chat/speaker, run/expanded-tool-detail, run/inline-thinking-item, run/run-row-shell and hooks/use-follow-tail modules directly. These entries preserve their broad-entry bindings without importing chat session stores.

Shared formatters

@tangle-network/ui/utils/format is a pure module for duration, clock, compact counts, USD, bytes and UTC labels. It loads without React. Charts re-exports these helpers for static figures and record views. formatDuration and formatUptime remain aliases of formatSpanDuration; chat timestamps use explicit UTC, and file/upload sizes share formatBytes.

HoverCard shares Popover positioning, portal and keyboard behavior. Its optional anchor accepts an existing DOM element for virtualized rows or canvas nodes. Hover delays default to the trigger's --duration-fast token. HoverCardContent and PopoverContent accept the local theme container, so previews preserve inherited tokens without copying computed styles.