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

@ghostystudio/widget

v0.2.1

Published

Ghosty Studio web widget for React: drop-in chat bubble (<GhostyWidget>) or headless hook (useGhostyChat) to draw your own chat.

Downloads

289

Readme

@ghostystudio/widget

The Ghosty Studio web widget for React. Put your agent on your site as the usual chat bubble, or draw your own chat with a headless hook. No dependencies besides React (≥ 18).

npm i @ghostystudio/widget

You need a widget token (emb_…): agent → Channels → Web widget, or ghosty agents embed enable <agent-id> --domains yoursite.com. It only works on the domains you authorize there (https only; yoursite.com includes www., *.yoursite.com its subdomains).

The bubble: <GhostyWidget>

import { GhostyWidget } from "@ghostystudio/widget";

<GhostyWidget token="emb_…" />

It mounts the same bubble + iframe as /widget.js and cleans up on unmount. It takes the look saved in Ghosty; props win over it:

| Prop | | |---|---| | position | "right" | "left" | | offsetX, offsetY | px from the side and the bottom (0–200) | | bubbleText | label next to the button (null hides it) | | defaultOpen · open + onOpenChange | uncontrolled / controlled | | style | the widget's style keys: fontFamily, radius, headerBg, headerText, titleUppercase, statusDot, headerAvatar, panelBg, userBubbleBg, userBubbleText, agentBubbleBg, agentBubbleText, inputBg, sendBg, sendIcon, sendArrow, launcherBg, launcherIcon, typing (bounce · flow · flashing · shimmer), typingColor (colors #rrggbb; validated by the server) | | copy | the phrases while the agent works: waking, connecting, thinking (1–8 that rotate), stillWorking, cantStart, interrupted, retryLabel (plain text ≤ 80), showDuration, showCopyButton. Win over the saved ones; validated by the server | | baseUrl | default https://www.ghosty.studio | | zIndex | default 2147483000 |

A law firm that doesn't want Ghosty's playful waiting phrases:

<GhostyWidget token="emb_…" copy={{ waking: "Un momento, por favor", thinking: ["Revisando su consulta…"], showDuration: false }} />

Headless: useGhostyChat

import { useState } from "react";
import { useGhostyChat } from "@ghostystudio/widget";

export function MyChat() {
  const { messages, send, stop, status, error } = useGhostyChat({ token: "emb_…" });
  const [text, setText] = useState("");
  const busy = status === "thinking" || status === "streaming";
  return (
    <div>
      {messages.map((m) => <p key={m.id} className={m.role}>{m.text}</p>)}
      {status === "thinking" && <p>…</p>}
      {error && <p role="alert">{error}</p>}
      <form onSubmit={(e) => { e.preventDefault(); send(text); setText(""); }}>
        <input value={text} onChange={(e) => setText(e.target.value)} />
        {busy ? <button type="button" onClick={stop}>Stop</button> : <button>Send</button>}
      </form>
    </div>
  );
}
  • messages: { id, role: "user" | "assistant", text, pending? } — the agent's text streams in.
  • status: "connecting" | "idle" | "thinking" | "streaming".
  • error: caps and notices as text ready to show.
  • copy: the owner's phrases while the agent works (agent → Channels → Web widget → Texts, or ghosty agents embed <id> --copy), complete with Ghosty's defaults — draw your own waiting state with copy?.thinking[0]. null until it loads.
  • The visitor is a uuid in localStorage (ghosty:widget:vid); their conversation comes back on return.

Requests go from your page to Ghosty with CORS: they only pass when the page's origin is one of the widget's domains. localhost is rejected — test on an authorized domain.

Changes in 0.2.0

  • <GhostyWidget copy> and useGhostyChat().copy: the phrases while the agent works.

No package at all

ghosty agents embed <agent-id> --out src/components/GhostyChat.tsx writes a complete, editable component into your project with your token and theme baked in (shadcn style).

Docs: ghosty.studio/docs/canales/embed · English