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

@codeam/ide-web

v0.11.1

Published

React (DOM) UI surface of @codeam/ide — Monaco-backed file viewer, file explorer, source control, search and (Phase 3) extension runtime. Drops into any React web app as components; consumers wire their own backend via the @codeam/ide-core adapter contrac

Downloads

2,965

Readme

@codeam/ide-web

A VS Code–style IDE surface for React (DOM). Activity bar, file explorer, source control, search, settings, tabs, breadcrumbs, inline editor, diff viewer, and terminal. Drops into any React web app — bring your own backend via small adapter interfaces.

npm License: MIT

Quick install

npm install @codeam/ide-web @codeam/ide-core
# Peer deps the package expects you already have:
npm install react react-dom

The package ships ESM + CJS and works with Vite, Next.js, Remix, Create React App, etc.

What you get

| Component | What it renders | Adapter it consumes | | ------------------------------ | --------------------------------------------------------------------- | ------------------------------- | | IDEShell | Activity bar + side panel + main content, responsive (drawer < 768px) | none — pure layout | | ActivityBar | Vertical icon strip (Files / Search / SCM / …) | none | | FileTreeSidebar | VS Code-style explorer with search + collapsible tree | FileTreeProvider | | SourceControlPanel | Branch header, commit input, file list, commit-log Graph | GitProvider | | SearchPanel | Multi-file search with case / word / regex toggles | SearchProvider | | SettingsPanel | Theme / font / wrap / minimap / line-numbers controls | SettingsStore (optional) | | TabsBar | Editor-tab strip with dirty markers | none — controlled by parent | | Breadcrumbs | Clickable path segments above the editor | none | | FileViewerProvider | Context + modal host that wraps Monaco for "open this file" | FileFetcher | | FileViewerHost | Fullscreen Monaco editor modal (chat use case) | reads from FileViewerProvider | | (consumer-side) InlineEditor | Inline Monaco editor for the IDE main pane (see example below) | FileFetcher + SettingsStore | | DiffViewer | Side-by-side Monaco diff (working tree ↔ HEAD) | GitProvider + FileFetcher | | TerminalPanel | xterm.js terminal with a "Recent commands" chip strip | TerminalProvider |

Every adapter type is re-exported from @codeam/ide-web so the consumer only ever imports from one place.


The five-minute integration

The smallest useful integration is a single file with one read endpoint:

import {
  FileViewerProvider,
  FileViewerHost,
  useFileViewer,
  type FileFetcher,
} from '@codeam/ide-web';

const fetcher: FileFetcher = {
  label: 'demo',
  canWrite: false,
  async read(path) {
    const r = await fetch(`/api/files?path=${encodeURIComponent(path)}`);
    if (!r.ok) return { error: `HTTP ${r.status}` };
    return { content: await r.text() };
  },
  async write() {
    return { error: 'Read-only demo' };
  },
};

function OpenButton() {
  const { open } = useFileViewer();
  return <button onClick={() => open({ path: 'README.md', op: 'Read' })}>Open</button>;
}

export function App() {
  return (
    <FileViewerProvider fetcher={fetcher}>
      <OpenButton />
      <FileViewerHost />
    </FileViewerProvider>
  );
}

That's it — clicking the button pops a Monaco-backed editor over your page.


Full IDE in 100 lines

A complete VS Code-style shell with the activity bar, file explorer, source control, settings, tabs, and inline editor:

import { useMemo, useState } from 'react';
import {
  Breadcrumbs,
  DiffViewer,
  FileTreeSidebar,
  IDEShell,
  SearchPanel,
  SettingsPanel,
  SourceControlPanel,
  TabsBar,
  type ActivityBarItem,
  type EditorTab,
  type FileFetcher,
  type FileTreeProvider,
  type GitProvider,
  type GitStatusEntry,
  type SearchProvider,
  type SettingsStore,
} from '@codeam/ide-web';
import { InlineEditor } from './InlineEditor'; // see below

type View = 'files' | 'search' | 'scm' | 'settings';

export function IDEPage({
  fetcher,
  fileTree,
  git,
  search,
  settings,
}: {
  fetcher: FileFetcher;
  fileTree: FileTreeProvider;
  git: GitProvider;
  search: SearchProvider;
  settings: SettingsStore;
}) {
  const [view, setView] = useState<View | null>('files');
  const [openTabs, setOpenTabs] = useState<string[]>([]);
  const [activeTab, setActiveTab] = useState<string | null>(null);
  const [buffers, setBuffers] = useState<Record<string, string>>({});
  const [saved, setSaved] = useState<Record<string, string>>({});
  const [diff, setDiff] = useState<{ path: string; staged: boolean } | null>(null);

  const openFile = (path: string) => {
    setOpenTabs((p) => (p.includes(path) ? p : [...p, path]));
    setActiveTab(path);
    setDiff(null);
  };

  const closeTab = (path: string) => {
    setOpenTabs((p) => p.filter((x) => x !== path));
    if (activeTab === path) setActiveTab(null);
  };

  const items: ActivityBarItem[] = useMemo(
    () => [
      { id: 'files', label: 'Explorer', icon: <FilesIcon /> },
      { id: 'search', label: 'Search', icon: <SearchIcon /> },
      { id: 'scm', label: 'Source Control', icon: <BranchIcon /> },
    ],
    [],
  );

  const tabs: EditorTab[] = openTabs.map((p) => ({
    id: p,
    label: p.split('/').pop() ?? p,
    dirty: buffers[p] !== undefined && saved[p] !== undefined && buffers[p] !== saved[p],
  }));

  return (
    <IDEShell
      activityItems={items}
      activityBottomItems={[{ id: 'settings', label: 'Settings', icon: <CogIcon /> }]}
      activeView={view}
      onViewChange={(id) => setView(id as View | null)}
      panels={{
        files: <FileTreeSidebar provider={fileTree} selectedPath={activeTab} onSelect={openFile} />,
        search: <SearchPanel provider={search} onOpen={(h) => openFile(h.path)} />,
        scm: (
          <SourceControlPanel
            provider={git}
            onSelect={(e: GitStatusEntry) => setDiff({ path: e.path, staged: e.staged })}
          />
        ),
        settings: <SettingsPanel store={settings} />,
      }}
    >
      <TabsBar tabs={tabs} activeId={activeTab} onSelect={setActiveTab} onClose={closeTab} />
      <Breadcrumbs path={diff?.path ?? activeTab ?? ''} rootLabel="workspace" />
      {diff ? (
        <DiffViewer
          path={diff.path}
          git={git}
          fetcher={fetcher}
          staged={diff.staged}
          onClose={() => setDiff(null)}
        />
      ) : (
        <InlineEditor
          fetcher={fetcher}
          path={activeTab}
          settingsStore={settings}
          buffers={buffers}
          setBuffers={setBuffers}
          saved={saved}
          setSaved={setSaved}
        />
      )}
    </IDEShell>
  );
}

Adapter recipes

You bring three small adapters: file ops, file-tree listing, and git. Search and terminal are optional. Adapter identity must be stable across renders — wrap creation in useMemo or use a module-level cache keyed by workspace id.

import type {
  FileFetcher,
  FileTreeProvider,
  GitProvider,
  SearchProvider,
  TerminalProvider,
} from '@codeam/ide-web';

export const fileFetcher: FileFetcher = {
  label: 'demo-workspace',
  canWrite: true,
  read: async (path) => fetch(`/api/files/${encodeURIComponent(path)}`).then(/* … */),
  write: async (path, body) =>
    fetch(`/api/files/${encodeURIComponent(path)}`, { method: 'PUT', body }).then(/* … */),
};

export const fileTree: FileTreeProvider = {
  list: async (query) =>
    fetch(`/api/files?query=${encodeURIComponent(query ?? '')}`).then((r) => r.json()),
};

export const git: GitProvider = {
  status: () => fetch('/api/git/status').then((r) => r.json()),
  diff: (p, staged) => fetch(`/api/git/diff?path=${p}&staged=${!!staged}`).then((r) => r.json()),
  stage: (paths) =>
    fetch('/api/git/stage', { method: 'POST', body: JSON.stringify({ paths }) }).then(() => {}),
  unstage: (paths) =>
    fetch('/api/git/unstage', { method: 'POST', body: JSON.stringify({ paths }) }).then(() => {}),
  commit: (opts) =>
    fetch('/api/git/commit', { method: 'POST', body: JSON.stringify(opts) }).then((r) => r.json()),
  push: () => fetch('/api/git/push', { method: 'POST' }).then((r) => r.json()),
  fetch: () => fetch('/api/git/fetch', { method: 'POST' }).then((r) => r.json()),
  // Optional — when present, the SCM panel renders a Graph section.
  log: async (limit) => fetch(`/api/git/log?limit=${limit ?? 30}`).then((r) => r.json()),
};

export const search: SearchProvider = {
  search: async (q, opts) =>
    fetch('/api/search', {
      method: 'POST',
      body: JSON.stringify({ query: q, ...opts }),
    }).then((r) => r.json()),
};

export const terminal: TerminalProvider = {
  open: async ({ cols, rows, cwd }) =>
    fetch('/api/terminal/open', { method: 'POST', body: JSON.stringify({ cols, rows, cwd }) }).then(
      (r) => r.json(),
    ),
  write: async (session, data) => {
    await fetch(`/api/terminal/${session.id}/write`, { method: 'POST', body: data });
  },
  resize: async (session, cols, rows) => {
    await fetch(`/api/terminal/${session.id}/resize`, {
      method: 'POST',
      body: JSON.stringify({ cols, rows }),
    });
  },
  subscribe: (session, handler) => {
    const es = new EventSource(`/api/terminal/${session.id}/stream`);
    es.addEventListener('data', (e) => handler({ type: 'data', data: (e as MessageEvent).data }));
    es.addEventListener('exit', (e) =>
      handler({ type: 'exit', exitCode: Number((e as MessageEvent).data) }),
    );
    return () => es.close();
  },
  close: async (session) => {
    await fetch(`/api/terminal/${session.id}`, { method: 'DELETE' });
  },
};

Persistence — SettingsStore

The SettingsPanel reads / writes through an optional SettingsStore. A localStorage implementation is ~50 lines and gives you cross-tab synchronisation for free:

import type { SettingsStore } from '@codeam/ide-web';

const PREFIX = 'codeam-ide:';

export const localStorageSettings: SettingsStore = {
  async get(key) {
    const raw = window.localStorage.getItem(PREFIX + key);
    return raw ? JSON.parse(raw) : undefined;
  },
  async set(key, value) {
    window.localStorage.setItem(PREFIX + key, JSON.stringify(value));
  },
  watch(cb) {
    const onStorage = (e: StorageEvent) => {
      if (e.key?.startsWith(PREFIX)) {
        cb(e.key.slice(PREFIX.length), e.newValue ? JSON.parse(e.newValue) : undefined);
      }
    };
    window.addEventListener('storage', onStorage);
    return () => window.removeEventListener('storage', onStorage);
  },
};

Pass it to the SettingsPanel and the InlineEditor (so settings changes apply live).


Tips

  1. Memoise adapter instances. The library keys its load effects off adapter identity. A fresh { list: () => fetch(...) } per render causes refetch loops + flicker.
  2. The SearchProvider is optional. Pass a stub returning { hits: [], truncated: false } if you don't have a backend yet; the panel still renders and reports "No results."
  3. The SCM Graph section is opt-in. Implement git.log only when you want it.
  4. IDEShell keeps non-active panels mounted (with display: none), so the FileTree's scroll position survives switching to Search and back. Same trick VS Code uses.
  5. Mobile drawer — pass mobileBreakpoint={600} to switch the breakpoint for tablet-sized phones.

Cross-platform

A React Native equivalent ships as @codeam/ide-native. Same prop shape, same adapter contracts — swap import paths.

// web
import { IDEShell, FileTreeSidebar } from '@codeam/ide-web';

// native
import { IDEShell, FileTreeSidebar } from '@codeam/ide-native';

Native uses xterm.js + Monaco inside a WebView (loaded from a CDN at runtime), so the package keeps the npm tarball lean and your consumer's Metro bundle small.


License

MIT © Edgar Durand