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

ink-cartridge

v5.4.0

Published

Enhances Ink without replacing it — foundational screen management and layered keyboard engine for building interaction-dense terminal UIs.

Readme

Table of Contents

Design Philosophy

ink-cartridge aims to enhance Ink, not replace it. It makes the interaction and development experience of Ink apps more comfortable, and the apps themselves more deterministic and stable, by providing the foundational capabilities for building interaction-dense, multi-page applications.

ink-cartridge is not an all-in-one framework. Most business logic is implemented by you or composed from other libraries; ink-cartridge only provides the foundational capabilities for building complex applications.

  • Screen as component — Any React component can be a screen. Register it into a tree and navigate with skip / back / gotoScreen. No hand-written conditional rendering.
  • Layered keyboard engine — Each screen owns its key bindings. A 9-stage pipeline resolves conflicts between modal layers, layers, global keys, and the screen stack. The focus system partitions keys within the same layer.

Note: The standalone component packages under packages/ (@cartridge-engine/*) are deprecated and will no longer be maintained. A rewritten component set will replace them.

Quick Start

import React, { useContext, useEffect, useState } from "react";
import { Box, Text, render } from "ink";
import {
	CurrentScreen,
	KeyboardProvider,
	ModalLayerElementContext,
	registerComponent,
	ScenarioManagementProvider,
	useKeyboard,
	useMouseRegion,
	useScreenSystem,
} from "ink-cartridge";

// ── Home ──
function Home() {
	const { skip, openModalLayer, applyElementToModalLayer } = useScreenSystem();
	const { boundKeyboard } = useKeyboard();

	useEffect(() => {
		const toProgress = boundKeyboard(["p"], () => skip(ProgressBar, {}));
		const modals = boundKeyboard(["m"], () => {
			const stacked = [
				["low", 1, 0, 80],
				["high", 2, 4, 70],
				["high-high", 3, 8, 60],
				["high-high-high", 4, 10, 55],
				["high-high-high-high", 5, 12, 50],
				["high-high-high-high-high", 6, 13, 45],
				["high-high-high-high-high-high", 7, 12, 40],
				["high-high-high-high-high-high-high", 8, 11, 35],
			] as const;

			for (const [layerId, zIndex, top, left] of stacked) {
				openModalLayer(layerId, zIndex);
				applyElementToModalLayer(layerId, {
					elementId: `${layerId}-modal`,
					element: () => <Modal top={top} left={left} />,
				});
			}
		});
		return () => {
			toProgress();
			modals();
		};
	}, [boundKeyboard]);

	return (
		<Box flexDirection="column">
			<Text bold>🏠 Home</Text>
			<Text>Press P to open Progress Bar</Text>
			<Text>Press M to open stacked modals</Text>
			<Text>Click Cancel on any modal to close it</Text>
		</Box>
	);
}
registerComponent(Home, {});

// ── Progress Bar ──
function ProgressBar() {
	const [value, setValue] = useState(50);
	const { back } = useScreenSystem();
	const { boundKeyboard } = useKeyboard();

	useEffect(() => {
		const left = boundKeyboard(["left"], () => setValue((v) => Math.max(0, v - 5)));
		const right = boundKeyboard(["right"], () => setValue((v) => Math.min(100, v + 5)));
		const esc = boundKeyboard(["escape"], () => back());
		return () => {
			left();
			right();
			esc();
		};
	}, [boundKeyboard]);

	// Smooth color gradient: red(0) → yellow(50) → green(100)
	const r = value <= 50 ? 255 : Math.round(255 - (value - 50) * 5.1);
	const g = value <= 50 ? Math.round(value * 5.1) : 255;
	const color = `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}00`;

	const filled = Math.round(value * 0.4);
	const bar = "█".repeat(filled) + "░".repeat(40 - filled);

	return (
		<Box height="100%" width="100%" justifyContent="center" alignItems="center" flexDirection="column">
			<Text dimColor>← → adjust  ·  Esc back</Text>
			<Text color={color}>{bar} {value}%</Text>
		</Box>
	);
}
registerComponent(ProgressBar, {}, { parent: Home });



function Modal({ top, left }: { top: number; left: number }) {
	const { closeModalLayer } = useScreenSystem();
	const { boundKeyboard } = useKeyboard();
	const modalCtx = useContext(ModalLayerElementContext);
	const layerId = modalCtx?.modalLayer.layerId;

	useEffect(() => {
		if (!layerId) return;
		return boundKeyboard(["escape"], () => closeModalLayer(layerId));
	}, [boundKeyboard, closeModalLayer, layerId]);

	return (
		<Box
			position="absolute"
			top={top}
			left={left}
			width={46}
			borderStyle="round"
			borderColor="yellow"
			padding={1}
			backgroundColor="black"
			flexDirection="column"
		>
			<Text bold color="yellow">
				⚠ cartridge.exe — Application Error
			</Text>
			<Text color="yellow">  Unhandled exception has occurred in your application.</Text>
			<Text color="yellow">  </Text>
			<Text color="yellow">  NullReferenceException:</Text>
			<Text color="yellow">  Object reference not set to an instance of an object.</Text>
			<Box flexDirection="row" justifyContent="flex-end" marginTop={1}>
				{/* Click-to-close button: a child region with a higher priority
				    wins over the modal body, so only the button reacts. */}
				<ModalButton
					label="Cancel"
					onPress={() => {
						if (layerId) closeModalLayer(layerId);
					}}
				/>
			</Box>
			<Text dimColor>Esc closes · click Cancel to close</Text>
		</Box>
	);
}

// A small clickable control. `priority: 1` makes it win over any region that
// contains it (the modal body would be a priority-0 region).
function ModalButton({ label, onPress }: { label: string; onPress: () => void }) {
	const [hovered, setHovered] = useState(false);
	const ref = useMouseRegion(
		{
			onClick: onPress,
			onEnter: () => setHovered(true),
			onLeave: () => setHovered(false),
		},
		{ priority: 1 },
	);
	return (
		<Box borderStyle="round" borderColor={hovered ? "green" : "gray"} marginLeft={1} ref={ref}>
			<Text>{label}</Text>
		</Box>
	);
}

render(
	<ScenarioManagementProvider defaultScreen={Home} fullScreen>
		<KeyboardProvider mouse>
			<CurrentScreen />
		</KeyboardProvider>
	</ScenarioManagementProvider>
);

Not only that, it also supports full coordination between mouse and keyboard focus. This makes terminal interaction smoother. Demo Example

Drag: Demo Example

With Ink-cartridge, you can achieve buttery-smooth window raising, taking the interactivity and visual quality of React Ink apps to the next level. On top of that, it keeps keyboard priority exactly aligned with window priority. As shown in the example below, when you press the X key, only the highest-priority window will respond. Demo Exmaple

Installation

npm install ink-cartridge

For the standalone keyboard engine (framework-agnostic):

npm install @cartridge-engine/keyboard-engine

Standalone packages — import only what you need:

npm install @cartridge-engine/i18n @cartridge-engine/theme @cartridge-engine/event

| Package | Exports | CLI bin | |---------|---------|---------| | @cartridge-engine/i18n | LanguageProvider, useI18n | make-language-type | | @cartridge-engine/theme | ThemeProvider, useTheme | make-theme-type, init-theme | | @cartridge-engine/event | EventBus, EventProvider, createEventBus, useEventBus, useEmitter, useSubscribe | — | | @cartridge-engine/init | project scaffold | ink-cartridge-init | | @cartridge-engine/ui | Button (in progress) | — |

Documentation

API docs are auto-published to GitHub Pages on every push to main:

For AI

AI-friendly project — see AGENTS.md for coding conventions, agents/rules/ for conditional rules, and docs-agents/ for reference material. AI writes, humans review and sign off.

Skills live in skills/ (SKILL.md per agent tool); install them for an agent with script/install-skill/<agent>.sh, e.g. script/install-skill/claude-code.sh.

Examples

Runnable demos for every component. See examples/README.md for the full list and run commands.

License

MIT