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

@devicechain/widgets

v0.17.0

Published

DeviceChain presentational dashboard widgets across five channels — telemetry (time-series chart + gauge over Apache ECharts, latest card, table, label, image), alarm (table, count), control (command button), selection (entity selector), and location (map

Readme

@devicechain/widgets

React dashboard widgets for DeviceChain, plus the renderer that lays a board out. Eleven widgets across five channels — charts, gauges, tables, alarm views, a command button, an entity selector and a map — all bound to live data through @devicechain/dashboards.

Themed entirely with CSS custom properties. No Tailwind, no global styles, no opinions about your app's shell — these are meant to be embedded.

npm install @devicechain/widgets @devicechain/dashboards @devicechain/client graphql react react-dom

Render a board

import { DashboardRenderer } from '@devicechain/widgets';
import { DashboardHub, parseDashboardDefinition } from '@devicechain/dashboards';

const hub = new DashboardHub({ resolver, authorities: user.scopes });
const definition = parseDashboardDefinition(json);

<DashboardRenderer definition={definition} hub={hub} actions={hub} />;

Omit actions for a strictly read-only mount and the acting widgets — alarm acknowledge, command send — render without their controls. That is the whole opt-in: a viewer that never passes actions cannot write, regardless of what the board contains.

Or drive a single widget yourself with ConnectedWidget, and look components up by type through WIDGET_REGISTRY and its per-channel siblings.

The widgets

| channel | widgets | | --- | --- | | measurement | timeseries-chart, gauge, latest-card, table, label, image | | alarm | alarm-table, alarm-count | | control | command-button | | selection | entity-selector | | location | map |

Charts and gauges are Apache ECharts; the map is MapLibre GL, loaded lazily so a board with no map on it downloads none of it.

WIDGET_OPTIONS is the typed schema for every widget's options bag — validateWidgetOptions reports exactly where a stored bag disagrees with what the renderer expects, which is what lets an authoring UI show a problem instead of rendering something wrong.

Rendering a map: one required piece of host wiring

MapLibre parses tiles in a web worker, and it derives that worker's URL at runtime from its own module URL — a computed string no bundler can trace. So no bundler emits the file, the browser 404s it, new Worker() does not throw, and the map renders as an empty box with nothing in the console. It is a famously quiet failure, and it is not something this package can paper over: only your bundler can emit that file.

So the worker URL comes from you, through a provider. On Vite that is one import:

import { MapRuntimeProvider } from '@devicechain/widgets';
import { viteMapRuntime } from '@devicechain/widgets/vite';

<MapRuntimeProvider runtime={viteMapRuntime}>
  <DashboardRenderer definition={definition} hub={hub} />
</MapRuntimeProvider>;

On any other bundler you supply the URL yourself. There is one requirement, and it is the whole of the difficulty: MapLibre loads that URL as a module worker, so whatever you serve there must be a module with no unresolved imports left in it. maplibre-gl/dist/maplibre-gl-worker.mjs is not one on its own — its first line imports a sibling, maplibre-gl-shared.mjs.

So do not point at a lone copy of the worker file, and in particular do not reach for new URL('maplibre-gl/dist/maplibre-gl-worker.mjs', import.meta.url). It looks right and it is not: webpack copies the file as an asset, the sibling is never emitted, and the worker dies on its own first line — with a 200 on the worker URL, a canvas on screen, markers placed, the build exited 0 and nothing in the console. The map just has no map in it.

Two recipes that work. On webpack, give the worker its own entry, so webpack bundles its imports into it:

entry: {
  main: './src/index.tsx',
  'maplibre-worker': 'maplibre-gl/dist/maplibre-gl-worker.mjs',
},
output: {
  filename: (data) =>
    data.chunk.name === 'maplibre-worker' ? 'maplibre-worker.js' : '[name].[contenthash].js',
},

…and point at the filename you chose:

const runtime = { workerUrl: '/maplibre-worker.js' };

Or, on any bundler at all, copy the two files yourself:

// BOTH, under their own names, into one served directory:
//   maplibre-gl/dist/maplibre-gl-worker.mjs  ->  public/vendor/
//   maplibre-gl/dist/maplibre-gl-shared.mjs  ->  public/vendor/
const runtime = {
  workerUrl: '/vendor/maplibre-gl-worker.mjs',
  loadStyles: () => import('maplibre-gl/dist/maplibre-gl.css'),
};

loadStyles is optional — import MapLibre's stylesheet yourself if you would rather have it eagerly. Supplying it here keeps the 83 KB (10.7 KB gzipped) on the map's lazy chunk, so a viewer who opens no map downloads none of it.

A map widget with no provider above it renders a visible notice, not a blank canvas. That is deliberate: an unwired host should be able to see what is missing.

maplibre-gl is a peer dependency, so your app and this package share exactly one copy. A worker built from one version driving a main thread on another is the same blank map by a different route.

Basemap tiles

TenantBasemapProvider supplies the tile source. It is genuinely optional — with no provider the map falls back to a plain view — and the resolution cascade lives in @devicechain/client so every DeviceChain surface resolves an override over the tenant default identically.

Theming

Every colour, radius and font comes from a CSS custom property, so a host restyles widgets by setting variables on any ancestor. The DeviceChain values ship as @devicechain/brand; nothing here requires them.

Compatibility

React 19, ESM only, built for bundler resolution (Vite, webpack, Rollup, esbuild). The emitted declarations use extensionless specifiers, which resolve under TypeScript's bundler module resolution and not under node16/nodenext.

License

Apache-2.0. See LICENSE.