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

@relyio/toolbar

v0.3.0

Published

The RelyIO preview toolbar: a development-only, shadow-DOM panel showing the dataset, branch, key and sandbox a page is reading from, and badging each bound field published or draft with a Studio link, over @relyio/sdk.

Readme

@relyio/toolbar

The RelyIO preview toolbar: a small floating panel, for development only, that shows which dataset, branch, key and relyio dev sandbox the page is reading from, and when that sandbox expires — and badges every field you bound with relyioField as published or draft in its locale, with a link into the Studio.

The preview key must never reach a production bundle. The toolbar holds a preview key in the browser, and a preview key reads drafts. Render the toolbar only in development, and read the key from an environment variable your production build does not set. The toolbar refuses to start under NODE_ENV=production, but that is a second line of defence, not the first: a key in your production JavaScript is readable by anyone whether or not the toolbar mounts.

npm install @relyio/toolbar @relyio/sdk

Next.js (App Router)

Render <RelyIOToolbar> from your root layout. The layout is a server component and a client cannot cross into a client component, so pass plain values: the preview key, the dataset, and the server client's sandbox — and baseUrl if your client sets one.

// app/layout.tsx
import { RelyIOToolbar } from '@relyio/toolbar/react'

import { relyio } from '@/lib/relyio'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang='en'>
      <body>
        {children}
        {process.env.NODE_ENV !== 'production' && (
          <RelyIOToolbar
            apiKey={process.env.RELYIO_PREVIEW_KEY!}
            dataset='main'
            sandbox={relyio.sandbox}
          />
        )}
      </body>
    </html>
  )
}

React single-page app

Pass the client you already created.

import { RelyIOToolbar } from '@relyio/toolbar/react'

{
  import.meta.env.DEV && <RelyIOToolbar client={relyio} />
}

Without React

mountToolbar from the package root is plain DOM. Import it dynamically so it stays out of your main bundle:

if (import.meta.env.DEV) {
  const { mountToolbar } = await import('@relyio/toolbar')
  const toolbar = mountToolbar({ client: relyio })
  // later: if (toolbar.state === 'mounted') toolbar.unmount()
}

What it shows

| Row | Shows | | ------- | ------------------------------------------------------------------------------------------------- | | Dataset | The sandbox's dataset when a relyio dev sandbox is active, otherwise the one you configured | | Branch | The sandbox's branch, or says the handshake named none, or the dataset's default | | Key | The key's scope prefix only — rly_prev_…. Never the key | | Sandbox | Active (and from where) with the relyio promote command, expired, unreadable, none, and more | | Expires | A countdown to the sandbox's deadline, or "no expiry" for a sandbox without one | | Studio | Whether the badges link to the Studio, or why not — "Studio URL not configured" among them | | Live | How many entries it follows for changes, or that it is reconnecting, resynced or stopped, and why |

Bound fields

The toolbar finds every element your page stamped with relyioField — on a server render as much as in the browser, because it reads the attributes off the page — and puts a badge on it:

| Badge | Meaning | | --------------------------------------- | ------------------------------------------------------------------------- | | published | The field's locale is published | | published, draft changes | Published, with unpublished edits in that locale — what a preview shows | | draft | The locale has never been published | | archived | The locale is archived | | no content in this locale | The locale has no content of its own; what you see is its fallback | | not found with this key | The key cannot read that entry | | reading…, unreadable (…), and a few | The read is in flight or failed, or the API reported something it doesn't |

Publication is per locale, not per field, so every field of one entry in one locale carries the same badge. Hovering a bound element outlines it. Each badge links to that entry, field, locale and dataset in the Studio; when the API has no Studio URL configured, the badges are plain text and the Studio row says so. An element missing any of the relyioField attributes is ignored.

A block is bound too. @relyio/react's <Blocks> and <RichText>, handed the field's binding (field={relyioField(page, 'sections')}), stamp each block they render with its key path — sections[_key=="f2k6ub9s"], and a callout inside it sections[_key=="f2k6ub9s"].body[_key=="c1"] — so each gets a badge, and its link takes you to that block rather than to the whole field.

What it reads

Only on a page with bound fields, and only reads, through @relyio/sdk:

  • each bound entry when it first appears, with GET /v1/{contentType}/{id}, and again whenever the changes feed names it;
  • the project once, with GET /v1/project, for the Studio URL;
  • the changes feed's current position, with one GET /v1/changes before the first entry read, so a change that lands while the page is being read is not missed;
  • the changes feed, with GET /v1/changes?ids=… for the entries on the page, from that position, once the first entry read has settled. A new bound entry reopens it with the wider set; unmount() aborts it. A page binding more entries than the API filters by id follows the whole dataset's feed instead and ignores changes to entries it does not show.

It never writes. The reads are made from the browser with the preview key, so the RelyIO API must admit your development origin.

Live updates

Each change the feed reports for an entry on the page is read again, its badges are redrawn, and then the toolbar dispatches a relyio:change event on window whose detail is the change — its entry id, content type, locale, kind and cursor, never the content. The name is RELYIO_CHANGE_EVENT from @relyio/sdk. @relyio/next's <RelyIORefresh /> listens for it and refreshes the route; any other page can do the same:

import { RELYIO_CHANGE_EVENT, type RelyIOChangeDetail } from '@relyio/sdk'

window.addEventListener(RELYIO_CHANGE_EVENT, event => {
  const change = (event as CustomEvent<RelyIOChangeDetail>).detail
})

Because the toolbar holds a preview key, draft saves reach it as well as publishes. When the feed's position has expired it reads every entry again, says so in the Live row, and then dispatches a relyio:resync event (RELYIO_RESYNC_EVENT) instead of a change, because the feed named none. Anything on the page may be stale then; <RelyIORefresh /> refreshes on it too:

import { RELYIO_RESYNC_EVENT, type RelyIOResyncDetail } from '@relyio/sdk'

window.addEventListener(RELYIO_RESYNC_EVENT, event => {
  const { cause } = (event as CustomEvent<RelyIOResyncDetail>).detail
})

When it does not start

mountToolbar returns { state: 'mounted', unmount() }, or { state: 'disabled', reason }:

| reason | When | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | production | Your build's NODE_ENV is production and you did not pass dev: true | | environment-unknown | Your build does not say what it is — no bundler replaced process.env.NODE_ENV, or it is unset — and you did not pass dev: true | | opted-out | You passed dev: false | | no-document | There is no DOM, as in a server render | | already-mounted | A toolbar is already on the page |

Next.js, webpack, Vite and esbuild all replace process.env.NODE_ENV in your bundle, so the production check works without configuration. A page served without a bundler has no NODE_ENV, so pass dev: true there — only in development.

Nothing mounts when you import the package.

Isolation

The panel is rendered inside a shadow root on one <relyio-toolbar> element appended to <body>. Your page's styles cannot reach it, its styles cannot reach your page, and it adds nothing to document.head or window (it dispatches relyio:change and relyio:resync there, and sets nothing). Badges and outlines are drawn inside that shadow root, over your page; nothing is written to your elements.

Licence

Apache-2.0