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

@proyectojacky/jacky

v0.1.1

Published

Jacky web pet plugin: framework-agnostic sprite engine + React wrapper with .jacky skin support (Workshop, URL, folder, drag & drop).

Readme

@proyectojacky/jacky

Web pet plugin for Jacky: a framework-agnostic sprite engine plus an optional React wrapper, with .jacky skin support (Workshop refs, URLs, folders, and drag & drop).

Live demo: jacky.club/plugin — try speech, movement, states, and skin switching in the browser.

npm install @proyectojacky/jacky

React / Next.js

import { JackyPet } from "@proyectojacky/jacky/react";

export default function App() {
  return (
    <JackyPet
      speed={2.5}
      walkMode="random"
      draggable
      dialogues={["¡Hola!", "¿Qué estás programando?"]}
      chatMode="auto"
      skin="proyecto_jacky/samplecharacter3"
    />
  );
}

Imperative handle

import { JackyPet, useJackyPet } from "@proyectojacky/jacky/react";

function App() {
  const { ref, handle } = useJackyPet({ skin: "/sprites/Jacky" });
  return (
    <>
      <button onClick={() => handle?.say("¡Formulario enviado!")}>
        Submit
      </button>
      <button onClick={() => handle?.setAnimation("Dance", { duration: 3000 })}>
        Dance
      </button>
      <JackyPet ref={ref} />
    </>
  );
}

Vanilla JS / Vue / Svelte / Astro / HTML

The root entry does not depend on React:

import { initJacky } from "@proyectojacky/jacky";

const jacky = initJacky({
  speed: 3,
  walkMode: "follow_cursor",
  onClick: (pet) => pet.say("¡Me hiciste clic!"),
});

document.getElementById("submit")?.addEventListener("click", () => {
  jacky.say("¡Listo!");
  jacky.setAnimation("Dance", { duration: 3000 });
});

Options (JackyOptions / JackyPetProps)

| Property | Type | Default | Description | | :--- | :--- | :--- | :--- | | speed | number | 2 | Pixels per frame at the skin's FPS. | | walkMode | 'random' \| 'follow_cursor' \| 'stationary' | 'random' | Locomotion behavior. | | draggable | boolean | true | Drag with the pointer. | | bounds | 'viewport' \| HTMLElement \| string \| BoundsObject | 'viewport' | Movement limits. | | size | number | 128 | Size in pixels. | | scale | number | 1 | Visual scale. | | zIndex | number | 9999 | Overlay z-index. | | initialPosition | { x?, y? } | center | Start position (number \| 'random' \| 'center' \| 'bottom-right' \| 'bottom-left'). | | dialogues | string[] \| DialogueDictionary | Spanish phrases | Phrases by event (greeting, idle, click, drag, petted). | | chatMode | 'auto' \| 'click_only' \| 'disabled' | 'auto' | When Jacky speaks. | | chatInterval | [minMs, maxMs] | [5000, 15000] | Auto-chat range. | | bubbleDuration | number | 4000 | Speech bubble duration (ms). | | skin | string \| SkinSourceConfig \| File \| Blob | /sprites/Jacky | Skin source. | | allowDropSkin | boolean | true | Drop .jacky files onto Jacky. | | skinPersistence | 'none' \| 'visitor' | 'none' | Persist visitor overrides (IndexedDB). | | skinScope | string | 'default' | Namespace for visitor overrides. | | fps | number | skin FPS | Override animation FPS. | | workshopApiBase | string | https://api.jacky.club | API used to resolve author/slug refs. | | onClick | (pet, event) => void | — | Click callback (default: GETTING_PET + petting). | | onStateChange | (newState, oldState) => void | — | PetState changes. | | onSkinLoaded | (skinInfo) => void | — | Fired when a skin is loaded. | | onError | (error) => void | console.error | Network / skin errors. |

Controller methods (JackyController)

Use the handle from useJackyPet() or the object returned by initJacky().

Speech bubbles — say / setBubble

Confirm a form submit, celebrate a download, or nudge the user toward a CTA.

// After a successful form submit
handle?.say("¡Formulario enviado!", 3500);

// setBubble is an alias (same bubble UI)
handle?.setBubble("Guardando…", 2000);

// Pair speech with a short happy animation
handle?.say("Deploy listo ✨");
handle?.setState(PetState.HAPPY, { duration: 2800 });

States & animations — setState / setAnimation / rerollAnimation

Mirror an AI agent: TYPING while waiting, HAPPY on success, ERROR on failure, DANCE for celebrations.

import { PetState } from "@proyectojacky/jacky/react";

// Logical state — picks a random variant + fallbacks
handle?.setState(PetState.TYPING, { duration: 5000 });
handle?.setState(PetState.DANCE, {
  duration: 4000,
  onFinish: () => handle?.say("¡Fin del baile!"),
});

// Named animation / alias (folder name, enum, snake_case…)
handle?.setAnimation("Dance", { duration: 3000 });
handle?.setAnimation("Getting Pet", { duration: 2500 });
handle?.setAnimation("taking_notes", { duration: 4000 });

// New random variant of the current state
handle?.rerollAnimation();

Skins that only ship Idle/ still support DANCE, WALKING, TYPING, and the rest via a fallback chain — missing animations degrade instead of throwing.

Movement — moveTo / setWalkMode

Spotlight a button in an onboarding tour, or pause wandering while Jacky explains something.

// Walk toward a UI target, then talk
const el = document.getElementById("cta");
const rect = el?.getBoundingClientRect();
if (rect) {
  await handle?.moveTo(rect.left, rect.top - 40, { speedOverride: 5 });
  handle?.say("¡Haz clic aquí!");
}

// Change locomotion live
handle?.setWalkMode("follow_cursor"); // chase the pointer
handle?.setWalkMode("stationary");    // stay put while explaining
handle?.setWalkMode("random");        // wander again

Skins — loadSkin / resetSkin / getSkinInfo

Let visitors try Workshop skins, or persist a personal override with skinPersistence="visitor".

// Workshop ref (author/slug) — ephemeral preview for this tab
await handle?.loadSkin("proyecto_jacky/samplecharacter3");

// Stable .jacky URL
await handle?.loadSkin("https://cdn.example.com/skins/otter.jacky");

// Folder pack (character.json + jacky.manifest.json)
await handle?.loadSkin({ folder: "/sprites/Jacky" });

// Visitor override — survives refresh if skinPersistence="visitor"
await handle?.loadSkin(fileFromInput, { persist: true });

// Back to the site skin (clears visitor override)
await handle?.resetSkin();

const info = handle?.getSkinInfo();
// { name, source: "site"|"visitor"|"preview", animations, fps, … }

Introspection — getState / getAnimationName / getPosition

Build a debug HUD, gate UI until IDLE, or sync analytics when Jacky reaches a target.

const state = handle?.getState();                 // PetState.IDLE
const anim = handle?.getAnimationName();          // requested key
const resolved = handle?.getResolvedAnimationName(); // after fallbacks
const { x, y } = handle?.getPosition() ?? { x: 0, y: 0 };

if (handle?.getState() === PetState.WALKING) {
  console.log("Jacky is walking to", x, y);
}

Live config & teardown — updateConfig / destroy

Tune speed/chat from a settings panel, then clean up when leaving an SPA route.

handle?.updateConfig({
  speed: 4,
  walkMode: "follow_cursor",
  chatMode: "click_only",
  dialogues: {
    click: ["¡Hey!", "¿Necesitas ayuda?"],
    idle: ["…"],
  },
});

// Tear down when leaving a route / SPA view
useEffect(() => {
  return () => handle?.destroy();
}, [handle]);

Compose several methods

Chain say + setState + moveTo + setAnimation for richer reactions.

async function celebrateDownload(name: string) {
  handle?.say(`¡Descargaste ${name}!`);
  handle?.setState(PetState.HAPPY, { duration: 2800 });
  await handle?.moveTo(
    window.innerWidth / 2 - 64,
    window.innerHeight / 2 - 64,
    { speedOverride: 6 },
  );
  handle?.setAnimation("Dance", { duration: 3500 });
}

Skins (.jacky)

| Layer | Who sees it | How it is installed | | :--- | :--- | :--- | | Site skin (A) | Every visitor | skin="author/slug" or a stable URL in your code | | Visitor override (B) | That browser only | Drop / loadSkin with skinPersistence="visitor" | | Ephemeral preview (C) | That tab only | Drop without persistence |

<JackyPet skin={{ workshop: "proyecto_jacky/samplecharacter3", version: "latest" }} />
<JackyPet skin="https://cdn.mysite.com/jacky/my-skin.jacky" />
<JackyPet skin={{ folder: "/sprites/Jacky" }} />

.jacky packages are zip archives. A package manifest.json is converted to skin metadata at load time; a ready character.json inside the zip also works.

Folder mode needs, next to the pack: character.json (metadata) and jacky.manifest.json (PNG listing — browsers cannot list directories):

{
  "name": "Jacky",
  "sprite_size": null,
  "fps": null,
  "sprite_facing": null,
  "animations": {
    "Idle": ["0_Bunny_Idle_000.png", "..."],
    "Walking": ["0_Bunny_Walking_000.png", "..."]
  }
}

Generate the manifest with:

pnpm --filter @proyectojacky/jacky gen:manifest -- ./path/to/pack

Product rule: what every visitor sees = layer A (Workshop ref / stable URL). Drops and File/Blob are layer B or C, never A.

Pack requirement

Same as Workshop: at least Idle with ≥ 4 PNGs. Without idle, loadSkin rejects with SkinLoadError and the engine keeps the previous skin.

Animation resolution

When you call setState / setAnimation, the engine:

  1. Maps the logical PetState to animation variants (STATE_ANIMATION_MAP)
  2. Picks one random variant per transition
  3. If frames are missing, walks a fallback chain (ANIMATION_FALLBACKS) down to idle
  4. If nothing resolves, keeps the previous frame

Tables (DIR_TO_STATES, STATE_ANIMATION_MAP, ANIMATION_FALLBACKS) and PetState are exported from the package for advanced use.

Development

pnpm install
pnpm --filter @proyectojacky/jacky build      # dist ESM + CJS + d.ts
pnpm --filter @proyectojacky/jacky test      # vitest
pnpm --filter @proyectojacky/jacky typecheck # tsc --noEmit