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

@tern-tui/react

v0.2.0

Published

tern react — react-reconciler custom renderer for tern

Readme

@tern/react

A mutation-mode react-reconciler custom renderer for tern. Renders React trees into the tern scene: host components map to @tern/core node factories, and every commit paints the terminal through the renderer.

Purpose

@tern/react is the React way to build a tern TUI. A component tree of host elements (<Box>, <Text>, <StreamingText>, ...) is mounted onto a core renderer's scene root; hooks provide the app handle, input, focus, resize, and theming. Bare string children are rejected at render time — text lives in an explicit <Text text="..." /> element.

API surface

Mounting

  • render(element, renderer) — create a root over renderer and render element immediately; returns a TernRoot (render / unmount).
  • createRoot(renderer) — create a root without rendering; call root.render(element) later.

Host components (props are @tern/core NodeProps plus optional role / component theme hints)

  • <Box> — container (border, background, padding, flex layout).
  • <Text text="..." /> — leaf node; string children are not allowed.
  • <StreamingText stream={AsyncIterable<Span>} autoScroll wrap /> — appends each span to the node and paints after every append; auto-scroll keeps scroll_y pinned to the stream tail.
  • <Input value caret placeholder focusId onChange onSubmit /> — with a focusId, registers with a FocusManager so routed keys edit it; a focused input auto-pastes routed pastes via pasteInto (firing onChange).
  • <Textarea lines row col width height scroll focusId onChange onSubmit /> — multi-line editor; with a focusId, routed keys edit it via editTextareaKey and routed pastes auto-paste via pasteIntoTextarea (firing onChange).
  • <Spinner value max width frames frame interval /> — determinate bar or indeterminate glyph; a tick interval (default 100ms) runs while mounted, skipping while the terminal is unfocused.
  • <StatusBar left center right /> — single-row segment strip.
  • <Panels panels={PanelSpec[]} active direction /> — collapsible header/body stack (bodies are core Nodes from element refs).
  • <DiffView hunks={DiffLine[]} mode inline_highlight scroll_x scroll_y wrap /> — unified or side-by-side diff rows (mode="side" two columns, inline_highlight intra-line char-level).
  • <Select options multi value highlighted filter open floating z_index focusId onChange onConfirm onDismiss /> — filterable option list.
  • <ScrollView clip_x clip_y clip_width clip_height scroll_x scroll_y showScrollbar> — clip/scroll region; content is the children.

Hooks

  • useApp(){ renderer, root, exit(error?), unmount() }; exit() unmounts the tree and tears the terminal down (idempotent).
  • useInput(handler, { isActive, focusManager }) — subscribe to key events; each key is routed through the FocusManager first, falling back to the tree handler.
  • usePaste(handler, { isActive, focusManager }) — subscribe to paste events (the handler receives the pasted text string); each paste is routed through the FocusManager first (routePaste), falling back to the tree handler. Teardown on unmount.
  • useFocus(id, nodeRef, onKey, { manager }) — register an element's node (from a ref) with a FocusManager.
  • useResize(handler) — subscribe to terminal resize; re-invokes renderer.render() after each so the compositor re-lays out.
  • usePanelMouseDrag(panelsRef) — wire mouse drag-resize for a <Panels> element (hit_test-gated gutter drags).
  • useTheme() / <ThemeProvider theme={ThemeOverrides}> — the active theme (partial themes merge over the core defaultTheme).

Re-exports — the core surface for typing props and wiring behavior: Node, NodeProps, KeyEvent, KeyHandler, Span, Renderer, FocusManager, focusManager, editKey, selectKey, tick, scrollTo / scrollBy / scrollTop, followTail / syncStreamTail / isStreamFollowing / scrollToBottom, pasteInto / pasteIntoTextarea, startPanelDrag / dragPanels / endPanelDrag, defaultTheme / mergeTheme / resolveTheme, and the theme types.

Example

import { createElement } from "react";
import { createRenderer } from "@tern/core";
import { Box, Text, render, useApp, useInput } from "@tern/react";

function App() {
  const { exit } = useApp();
  useInput((event) => {
    if (event.name === "char" && event.char === "q") exit();
  });
  return createElement(
    Box,
    { border_style: "rounded", padding: 1 },
    createElement(Text, { text: "Hello tern" }),
  );
}

const renderer = createRenderer({ exitOnCtrlC: true });
render(createElement(App), renderer);

// Passive effects (useInput's subscription) settle on the scheduler first.
await new Promise((resolve) => setTimeout(resolve, 100));
while (!renderer.destroyed) {
  renderer.pollEvents(50);
}

Runtime

Deno-first (deno check / deno test are canonical); the native addon requires --allow-ffi under Deno. Peers: react ^19.2.0 and react-reconciler ^0.33.0. See the @tern/core README for building the native addon and docs/guide.md for the component and event-model guides.