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

@joycostudio/studio

v0.1.1

Published

Plug project-specific tools into a /studio route, a floating debug pane, and MCP agents.

Readme

@joycostudio/studio

Plug project-specific tools into a /studio route, the app's floating debug pane, and MCP agents, with one contract per tool.

For contributor instructions on stable releases, betas, and commit previews, see the release workflow guide.

A tool's controls are an Xyz debug schema (Tweakpane). That same schema:

  • renders the inspector in /studio,
  • mounts in the app's floating pane (?debug / Option+D) through Xyz's DebugManager,
  • validates every write in the runtime (slider bounds, select options, types, and the shape of initial).

1. Define a tool

Tool files are loaded by the runtime in Node too, so keep them free of DOM/three imports and @/ aliases.

// src/tools/grading.ts
import { defineTool } from '@joycostudio/studio'
import { property, select, slider } from '@joycostudio/xyz/debug'
import initial from './grading.json'

export const grading = defineTool({
  id: 'color-grading',
  file: 'src/tools/grading.json', // where saves go, repo-relative
  initial, // the same file, imported: production ships the saved config
  preview: '/', // route shown next to the inspector in /studio
  controls: (config) => ({
    exposure: slider(property(config, 'exposure'), { min: 0, max: 3 }),
    tonemap: select(property(config, 'tonemap'), { ACES: 'aces', Filmic: 'filmic' }),
  }),
})

2. Register tools with the runtime

// studio.config.ts (its directory is the project root)
import { defineConfig } from '@joycostudio/studio'
import { grading } from './src/tools/grading'

export default defineConfig({ tools: [grading] })
{ "scripts": { "dev": "studio dev -- next dev" } }

studio dev starts the WebSocket bridge on ws://127.0.0.1:5179 and the HTTP MCP endpoint on http://127.0.0.1:5179/mcp (--port changes both). It stops when the wrapped command exits.

3. Use it in the app

// src/studio.ts
import { createStudio } from '@joycostudio/studio/client'

export const studio = createStudio({
  tools: [grading],
  runtime: process.env.NODE_ENV === 'development' && 'ws://127.0.0.1:5179',
})
const tool = studio.tool(grading)
apply(tool.config) // a live object: read it in your render loop…
tool.subscribe(apply) // …or react to every change, local or remote

Live edits also work without studio dev, including in production. Studio shares configs between same-origin previews and tabs in the same browser using BroadcastChannel. Peers match by tool ID, config file, and runtime URL (or no runtime); a newly opened or reloaded preview receives the current config from an open peer. Nothing is persisted: closing every page resets these edits to the shipped config. Save, Discard, and MCP still require a runtime. If a configured runtime disconnects, browser sync continues; reconnecting restores the runtime's draft.

Floating debug pane, with the same controls (plus save/discard when the runtime is up):

import { everyFrame } from '@joycostudio/studio/client'
import { DebugManager } from '@joycostudio/xyz/debug'
import { Pane } from 'tweakpane'

const debug = new DebugManager({ createPane: () => new Pane(), binding: { registerFrame: everyFrame } })
for (const tool of studio.tools) debug.register(tool)

The /studio route:

// app/studio/page.tsx
'use client'
import { Studio } from '@joycostudio/studio/react'
import { studio } from '@/studio'

export default function StudioPage() {
  return <Studio studio={studio} />
}

Use the pin button beside a sidebar tool to keep it at the top. Pinned tools also appear first in the Command/Ctrl+K menu. Pins are saved in this browser for the current origin and shared across tabs; unpinning restores registration order. Pinning a tool keeps the current preview selected.

In Studio, Command+B (Mac) or Ctrl+B slides both side panels out of view; press it again to bring them back. This also works from the same-origin preview iframe. The sidebar's collapse/expand button controls its width separately, and that choice is preserved when hiding and showing the panels. Move the pointer near either screen edge to reveal a small tab; click it to bring both panels back. The tabs also appear on keyboard focus and stay visible on touch devices. With reduced motion enabled, panels toggle instantly.

Previews use the shared PreviewCanvas: Reload and the tool title sit on the left, with Expand and header pinning on the right. Expand/Contain keeps the iframe and its scene mounted. Headers start pinned; enabling auto-hide overlays the header and hides it after three seconds away. Hover, keyboard focus, and open menus keep it visible, and touch devices always show the controls. Contained previews fit between the side panels, with the inspector stacked below on narrow screens.

Shared canvas and panel components

Import these from @joycostudio/studio/react with @joycostudio/studio/styles.css:

| Component | Use | | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | StudioSurface, StudioToolbar | Shared inset card and compact action row, including button states. | | CanvasToolbar | Pinned/auto-hide header; label, optional expanded/onToggle, and child controls. | | PreviewCanvas | Iframe panel with reload, expansion, and a toolbar slot replacing the title. | | StudioSelect, StudioSelectItem | Labeled Radix select with Studio field and portaled menu styling. | | StudioInput | Standard input with logical padding (ps-*/pe-*) for icon-bearing fields. | | StudioPanel, StudioPanelHeader, StudioPanelContent, StudioPanelFooter | Floating panel surface, inset sections, and scrolling body used by Studio's inspector. |

PreviewCanvas handles expansion itself, or accepts controlled expanded/onExpandedChange props. Native iframe props (including className, onLoad, and title) and its ref reach the actual iframe; onReload runs before an explicit reload. StudioSurface and StudioPanel support asChild to reuse their appearance on semantic elements without an extra wrapper. Panel layout is caller-owned.

The package owns the styles and interactions extracted from the BZRP Studio workspace, including 6px toolbar padding, 4px gaps, 8px padding on title text, and continuous sidebar header/footer surfaces. Consumers can replace local copies of the surface, toolbar, preview, select, and input components with these imports. Module graphs, tracing, and scene-specific controls remain host-owned.

Host preview and source-link extensions

Studio accepts previewComponent and getConfigFileHref, so integrations no longer need to patch the built package. Keep a custom preview component at module scope and forward its iframe ref and props; Studio uses them for keyboard shortcuts, comparison, and tool-switch lifecycle:

import { forwardRef, type ComponentPropsWithoutRef } from 'react'
import { PreviewCanvas, Studio } from '@joycostudio/studio/react'

const ProjectPreview = forwardRef<HTMLIFrameElement, ComponentPropsWithoutRef<'iframe'>>(
  (props, ref) => <PreviewCanvas {...props} ref={ref} />,
)

<Studio
  studio={studio}
  previewComponent={ProjectPreview}
  getConfigFileHref={(file) => new URL(file, sourceBaseUrl).href}
/>

The source base URL belongs to the host: it can point to a source browser or a development-only vscode://file/ URL for the current checkout. Without the callback, config paths remain plain text. See the playground's /studio/components route for controlled expansion, custom toolbar fields, and source links in use.

For a tool that already renders its own panels and headers, set previewLayout: 'custom' in its defineTool definition. Studio uses a bare iframe fitted to the available panel area, avoiding route-specific checks such as /studio/modules. A supplied previewComponent takes precedence. Use previewComponent="iframe" to retain the original full-window preview behavior for all tools.

When migrating the BZRP integration, remove its custom-preview/config-link pnpm patch, use these exports, and remove the generic shell/toolbar CSS that Studio now supplies. Keep application-specific module runtime expansion, source indexing, and session-local inspector adapters in the host.

Jump to tools from anywhere

Mount StudioCommandMenu once in the host app's root layout so it stays available on every route, including /studio:

// src/command-menu.tsx
'use client'
import '@joycostudio/studio/command-menu.css'
import { StudioCommandMenu } from '@joycostudio/studio/react'
import { studio } from './studio'

export function CommandMenu() {
  return <StudioCommandMenu studio={studio} />
}

Render <CommandMenu /> alongside the layout's {children}. The host owns mounting it, just like the floating debug pane. Its stylesheet is scoped to the menu and can be imported globally without applying Studio's page styles to the host app.

Press Command+K (Mac) or Ctrl+K to open or close it, type a tool name or ID to search, use ↑/↓ and Enter to select, or Escape to dismiss. Clicking a result also opens it. Results link to /studio#tool-id; switching tools from inside Studio changes the hash without reloading the page. The shortcut also works while focus is in Studio's same-origin preview iframe, opening the menu above Studio rather than inside the preview.

Pass studioPath="/custom/studio" for another route. For client-side navigation with a host router, pass its navigation function, for example <StudioCommandMenu studio={studio} navigate={router.push} /> with Next.js useRouter(). Without a router callback, navigation from another route uses the browser. The menu works without a runtime connection.

To run its browser tests, start the playground and run STUDIO_TEST_APP_URL=http://localhost:3000 pnpm --filter @joycostudio/studio exec vitest run test/command-menu.test.ts. Install Chromium first with pnpm exec studio install-browser if needed.

4. Connect an MCP agent

Start the app with studio dev -- <your dev command> as above. Add a Streamable HTTP MCP server to your agent with this URL:

http://127.0.0.1:5179/mcp

Use the URL printed by the CLI when running on a different port. Each project's runtime needs its own port when several projects run at once. The endpoint is local to the machine running Studio; a remote/cloud agent cannot reach it through its own localhost.

The MCP endpoint shares the running app's drafts. It supports both the 2025 MCP initialization flow and the 2026 MCP discovery flow, with no separate runtime or MCP process to launch.

| MCP tool | Arguments | Result | | ------------------------ | ---------------------------------------------- | ---------------------------------------------------------------- | | studio_list_tools | none | Registered IDs, titles, file paths, and preview routes | | studio_get_tool | tool | Complete draft, revision, saved value, dirty and conflict status | | studio_update_tool | tool, revision, value | Validated draft; all connected previews update immediately | | studio_save_tool | tool, revision, optional force | Draft persisted to its registered JSON file | | studio_discard_tool | tool, revision | Draft reset to the runtime's latest saved value | | studio_capture_preview | tool, optional revision, width, height | PNG image and draft revision from a fresh preview page |

Read before editing, copy data.draft.value, change the intended fields, then pass the complete config and data.draft.revision to studio_update_tool. Saving is separate. Use the new revision returned by the update to save or discard. If the playground or another agent edits in between, a stale_revision error tells you to read again and reconcile the changes. force bypasses a disk conflict, never a stale revision.

Every tool returns the runtime's { ok: true, data } or { ok: false, error, message? } as structured content and JSON text. Runtime failures also set MCP's isError, so invalid configs, unknown tools, stale revisions, and disk conflicts remain visible to the agent. Expected failures leave the draft intact.

Preview images

Set the running app's URL once in studio.config.ts (or override it with studio dev --app-url <url>):

export default defineConfig({ tools: [grading], appUrl: 'http://localhost:3000' })

Install Chromium once with pnpm exec studio install-browser. Then call studio_capture_preview with a tool ID. The existing preview route must use the Studio client connected to this runtime. No capture code or readiness hooks are needed in tool definitions, HTML components, or canvas renderers.

Capture starts a shared headless Chromium browser on first use and opens an isolated page for each request. It reads the live unsaved draft, waits for the client to receive that revision, then waits for fonts, current HTML images, and two animation frames. CSS animations are disabled for the screenshot. This is automatic settling, not a guarantee that arbitrary async work, delayed UI, or WebGL assets have finished. A draft change during capture returns stale_revision instead of an image. Simultaneous captures return capture_busy; retry after the active capture finishes.

The result includes a PNG as MCP image content and metadata with tool, revision, url, width, height, and settling: 'automatic'. The viewport defaults to 1200×800 at 1× scale; width accepts 320–2560 and height accepts 240–1600. The page starts fresh: it does not inherit your open tab's login, scroll, or interaction state. Preview routes and top-level navigation must stay on the configured app origin. Pages that never connect or settle time out.

For the browser integration tests, run pnpm exec studio install-browser and pnpm --filter @joycostudio/studio test:capture. The regular test suite runs setup validation without requiring Chromium.

The server binds to 127.0.0.1 by default and the MCP endpoint checks localhost Host headers and the same browser Origin allowlist as the WebSocket bridge. Native agent clients do not need an Origin header. This is a local development endpoint without authentication; do not expose it through a public proxy.

How it fits together

  • Runtime (studio dev) is the only file writer. It validates every update, keeps a draft revision per tool, writes atomically, refuses paths outside the project (symlinks included), only accepts localhost browser origins, and watches files for external edits (an external edit over unsaved work becomes a conflict that needs discard or force save).
  • Bridge: WebSocket (tools.list, tool.get, tool.update, tool.save, tool.discard + state broadcasts) and HTTP MCP on the same port. /studio, every app tab, and MCP all go through these same runtime operations.
  • Without a runtime (production or offline), browser messages synchronize live configs between same-origin Studio pages, previews, and app tabs. Subscriptions and Tweakpane controls still update; nothing persists. Browsers without BroadcastChannel retain same-page tweaking.

Run pnpm --filter @joycostudio/studio test:browser to verify runtime-free pane edits, iframe/tab sync, preview reloads, and runtime disconnect/reconnect in Chromium. Install Chromium first with pnpm exec studio install-browser if needed.