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

@meterry/component-vue

v0.2.3

Published

Vue 3 components for Meter data.

Readme

Meter Components

A headless UI component suite for metering data, published as several npm packages:

  • @meterry/component: framework-agnostic core with the Subject API client, table controller, chart model, and shared types.
  • @meterry/component-react: React components.
  • @meterry/component-vue: Vue 3 components.
  • @meterry/component-tailwindcss: Tailwind CSS plugin and default component styles.

This project does not use Web Components, Shadow DOM, or customElements. Component behavior is managed by React/Vue, styles are injected through Tailwind classes, and users can override any component slot.

Load-more tables

Use MeterUsageList (React / Vue) for automatic Subject queries with append-only Load more navigation. It defaults to event details, manages cursors and buffering, and needs only a client/provider plus optional query. MeterList requires columns (the same column definitions as Table) and handles existing data, custom page load, or a controlled list controller. Both render a shared table header and append rows with Load more, rather than rendering item cards. See the List guide for minimal integrations and a live demo. Existing table pagination APIs remain unchanged.

Installation

React:

npm install @meterry/component @meterry/component-react echarts echarts-for-react

Vue 3:

npm install @meterry/component @meterry/component-vue echarts vue-echarts

If you use Tailwind CSS:

npm install @meterry/component-tailwindcss

Automatic Usage Charts

MeterUsageChart queries, aggregates and renders without application loaders, row mapping or series definitions. React and Vue share the same core:

import { MeterClientProvider, MeterUsageChart } from '@meterry/component-react'

<MeterClientProvider tokenEndpoint="/api/subject-token" options={{ baseUrl: '/meter' }}>
  <MeterUsageChart type="line" query={{ metrics: ['input_tokens', 'output_tokens'], group_by: ['bucket', 'model'] }} />
  <MeterUsageChart type="doughnut" query={{ metrics: ['total_tokens'], group_by: ['model'] }} />
</MeterClientProvider>

Trend charts preserve metric/dimension series and sum duplicate buckets. Category charts sum requested groups for one additive metric/measure. Queries share a short-lived per-client cache; retry, refresh and cancellation are built in. Potentially truncated results are hidden by default, not misrepresented as a complete distribution. Existing data-only MeterChart remains supported. See docs/guide/usage-chart.md for details and the interactive five-type demo.

Automatic Usage Table

Use MeterUsageTable when you want the component to query and render usage without writing loaders, row mapping, cursor handling or state management:

import { MeterClientProvider, MeterUsageTable } from '@meterry/component-react'

<MeterClientProvider
  tokenEndpoint="/api/subject-token"
  options={{ baseUrl: '/meter', credentials: 'include' }}
>
  <MeterUsageTable source="events" rangeDays={7} pageSize={20} />
</MeterClientProvider>

The token endpoint is your session-authenticated backend and returns a Subject Token grant. Existing client/getJwt provider configuration remains supported. The Vue package exports the same component; configure an ancestor with provideMeterClient({ tokenEndpoint, options }).

  • source="events": automatically follows after_id/next_after_id as needed, reuses cached batches, handles unknown totals, cancellation, retry and refresh.
  • source="usage" (default): queries aggregate rows, generates columns from query.group_by/query.measures, and pages the returned result. The aggregate API currently declares no cursor; reaching the row limit displays a warning instead of pretending the returned prefix is complete.
  • No global sorting is advertised for endpoints without server-side sorting.
  • MeterTable remains available for advanced data/load/controller integration.

See docs/guide/usage-table.md for the complete React/Vue API and live demo.

Tailwind CSS Styles

Register the plugin in your Tailwind config:

import meterPlugin from '@meterry/component-tailwindcss'

export default {
  content: ['./src/**/*.{js,ts,jsx,tsx,vue}'],
  plugins: [meterPlugin],
}

Components emit semantic classes by default, such as meter-card, meter-table, and meter-chart. You can override the root node directly via className, or attach Tailwind classes to the stable slots via classNames:

Tailwind generates styles on demand: the meter-* class-name strings live in the core package source, so Tailwind must be able to scan them. For Tailwind v4, add this to your CSS:

/* While developing inside this repository */
@source "../node_modules/@meterry/component/dist/index.js";

(Adjust the path relative to your page's CSS file; the examples in this repo use a vite alias and scan src/component.ts directly.)

// rows: fetched from the meter service (see the React example below)
<MeterTable
  className="w-full"
  classNames={{
    card: 'rounded-2xl border-slate-200 shadow-xl',
    header: 'bg-slate-50 px-6 py-5',
    title: 'text-slate-950',
    table: 'overflow-hidden rounded-xl',
    button: 'rounded-lg border-indigo-200 text-indigo-700',
  }}
  columns={columns}
  data={rows}
/>

To skip the default semantic classes entirely:

// rows: fetched from the meter service (see the React example below)
<MeterChart
  unstyled
  classNames={{
    root: 'block',
    card: 'rounded-xl bg-white p-6 shadow',
    chart: 'mt-4',
  }}
  type="line"
  data={rows}
  x="bucket"
  series={[{ key: 'tokens', label: 'Tokens', value: 'tokens' }]}
/>

React

import { useEffect, useState } from 'react'
import {
  MeterChart,
  MeterClientProvider,
  MeterTable,
  MeterUsage,
} from '@meterry/component-react'
import { SubjectClient } from '@meterry/component'
import type {
  SubjectTokenGrant,
  TableColumn,
  TableQuery,
  TableResult,
} from '@meterry/component'

async function getSubjectToken({
  signal,
}: {
  signal?: AbortSignal
} = {}): Promise<SubjectTokenGrant> {
  const response = await fetch('/api/meter/subject-token', {
    credentials: 'include',
    headers: { accept: 'application/json' },
    signal,
  })
  if (!response.ok) {
    throw new Error(`Subject Token request failed: ${response.status}`)
  }
  return (await response.json()) as SubjectTokenGrant
}

// Shared client: components under the Provider reuse it automatically,
// and page code also uses it directly to fetch data from the meter service.
const client = new SubjectClient(getSubjectToken, { credentials: 'include' })

type UsageRow = { date: string; model: string; quantity: number }

const columns: TableColumn<UsageRow>[] = [
  { key: 'date', header: 'Date', sortable: true },
  { key: 'model', header: 'Model', sortable: true },
  { key: 'quantity', header: 'Quantity', align: 'right', sortable: true },
]

// MeterTable pulls rows from usage/query through load: filter values are
// forwarded to the service, while sorting and paging are applied to the
// returned rows inside load.
async function loadUsageRows(query: TableQuery): Promise<TableResult<UsageRow>> {
  const end = Math.floor(Date.now() / 1000)
  const response = await client.usageQuery({
    bucket_size: '1d',
    start_time: end - 14 * 86_400,
    end_time: end,
    metrics: ['input_tokens', 'output_tokens', 'total_tokens'],
    group_by: ['bucket', 'model', 'metric'],
    measures: ['quantity'],
    filters: query.filters,
    limit: 500,
  })
  const rows: UsageRow[] = response.rows.map((row) => ({
    date: String(row.dimensions?.bucket ?? ''),
    model: String(row.dimensions?.model ?? ''),
    quantity: Number(row.measures?.quantity ?? 0),
  }))
  return {
    rows: rows.slice(
      (query.page - 1) * query.pageSize,
      query.page * query.pageSize,
    ),
    total: rows.length,
  }
}

// MeterChart renders the rows you queried with the client:
// analysis/query -> map to rows -> data.
function DailyTokensChart() {
  const [rows, setRows] = useState<
    { bucket: string; input_tokens: number; output_tokens: number }[]
  >([])

  useEffect(() => {
    const end = Math.floor(Date.now() / 1000)
    client
      .analysisQuery({
        bucket_size: '1d',
        start_time: end - 14 * 86_400,
        end_time: end,
        metrics: ['input_tokens', 'output_tokens'],
        group_by: ['bucket'],
        measures: ['quantity'],
      })
      .then((response) => {
        const byBucket = new Map<string, { bucket: string; input_tokens: number; output_tokens: number }>()
        for (const series of response.series) {
          const metric = series.dimensions?.metric
          if (metric !== 'input_tokens' && metric !== 'output_tokens') continue
          for (const point of series.points) {
            const row = byBucket.get(point.bucket) ?? {
              bucket: point.bucket,
              input_tokens: 0,
              output_tokens: 0,
            }
            row[metric] = Number(point.value)
            byBucket.set(point.bucket, row)
          }
        }
        setRows([...byBucket.values()])
      })
      .catch((error) => console.error('Failed to load usage', error))
  }, [])

  return (
    <MeterChart
      type="line"
      data={rows}
      x="bucket"
      series={[
        { key: 'input', label: 'Input tokens', value: 'input_tokens' },
        { key: 'output', label: 'Output tokens', value: 'output_tokens' },
      ]}
    />
  )
}

export function BillingPage() {
  return (
    <MeterClientProvider client={client}>
      {/* Query the meter service yourself */}
      <MeterUsage
        title="Usage"
        classNames={{
          card: 'rounded-2xl border bg-white shadow-sm',
          value: 'text-4xl font-semibold text-indigo-600',
        }}
      />
      <MeterTable title="Usage events" columns={columns} load={loadUsageRows} />
      <DailyTokensChart />
    </MeterClientProvider>
  )
}

getJwt should call your application backend, which issues a short-lived Subject Token based on the logged-in session. MeterClientProvider creates a single shared client at the application level; multiple components under the Provider share the same token cache and any in-flight JWT request, so only one token request happens at a time. You can also create a SubjectClient explicitly and pass it to multiple components via client.

MeterUsage still accepts getJwt and baseUrl to create a client automatically, for backwards compatibility; if a page hosts multiple data components, prefer the Provider or a shared client instead of letting each component create its own client. MeterTable rows usually come from the meter service: prefer forwarding the query and filters to the service via load, or hand already-fetched rows to the built-in controller as local data — both keep the original filtering, sorting, and paging capabilities.

If you cannot use a Provider yet, pass the same tokenScope to all components so the compatibility mode shares a single provider:

<MeterUsage getJwt={getSubjectToken} tokenScope="meter:current-user" />
<MeterUsage getJwt={getSubjectToken} tokenScope="meter:current-user" />

tokenScope must be isolated per user, tenant, and environment. When a user logs out or switches accounts, call clearSharedSubjectToken(tokenScope). Creating a new client/provider alone does not clear persisted credentials; use a different authentication-scoped storage key or clear the old credentials first. If you use custom persistence, pass the same tokenStorage / tokenStorageKey options when clearing a scope after a page reload.

Vue 3

<script setup lang="ts">
import { onMounted, ref } from 'vue'
import {
  MeterChart,
  MeterTable,
  MeterUsage,
  provideMeterClient,
} from '@meterry/component-vue'
import { SubjectClient } from '@meterry/component'
import type { SubjectTokenGrant, TableColumn, TableQuery, TableResult } from '@meterry/component'

async function getSubjectToken(): Promise<SubjectTokenGrant> {
  const response = await fetch('/api/meter/subject-token', { credentials: 'include' })
  if (!response.ok) throw new Error(`Subject Token request failed: ${response.status}`)
  return (await response.json()) as SubjectTokenGrant
}

// Shared client: MeterUsage reuses it automatically, and page code also
// uses it directly to fetch data from the meter service.
const client = new SubjectClient(getSubjectToken, { credentials: 'include' })
provideMeterClient({ client })

const classNames = {
  card: 'rounded-2xl border bg-white shadow-sm',
  value: 'text-4xl font-semibold text-indigo-600',
}

type UsageRow = { date: string; model: string; quantity: number }

const columns: TableColumn<UsageRow>[] = [
  { key: 'date', header: 'Date', sortable: true },
  { key: 'model', header: 'Model', sortable: true },
]

// MeterTable pulls rows from usage/query through load: filter values are
// forwarded to the service, while sorting and paging are applied to the
// returned rows inside load.
async function loadUsageRows(query: TableQuery): Promise<TableResult<UsageRow>> {
  const end = Math.floor(Date.now() / 1000)
  const response = await client.usageQuery({
    bucket_size: '1d',
    start_time: end - 14 * 86_400,
    end_time: end,
    metrics: ['input_tokens', 'output_tokens', 'total_tokens'],
    group_by: ['bucket', 'model', 'metric'],
    measures: ['quantity'],
    filters: query.filters,
    limit: 500,
  })
  const rows: UsageRow[] = response.rows.map((row) => ({
    date: String(row.dimensions?.bucket ?? ''),
    model: String(row.dimensions?.model ?? ''),
    quantity: Number(row.measures?.quantity ?? 0),
  }))
  return {
    rows: rows.slice(
      (query.page - 1) * query.pageSize,
      query.page * query.pageSize,
    ),
    total: rows.length,
  }
}

// MeterChart renders the rows you queried with the client:
// analysis/query -> map to rows -> data.
const trend = ref<{ bucket: string; input_tokens: number; output_tokens: number }[]>([])

onMounted(async () => {
  const end = Math.floor(Date.now() / 1000)
  const response = await client.analysisQuery({
    bucket_size: '1d',
    start_time: end - 14 * 86_400,
    end_time: end,
    metrics: ['input_tokens', 'output_tokens'],
    group_by: ['bucket'],
    measures: ['quantity'],
  })
  const byBucket = new Map<string, { bucket: string; input_tokens: number; output_tokens: number }>()
  for (const series of response.series) {
    const metric = series.dimensions?.metric
    if (metric !== 'input_tokens' && metric !== 'output_tokens') continue
    for (const point of series.points) {
      const row = byBucket.get(point.bucket) ?? {
        bucket: point.bucket,
        input_tokens: 0,
        output_tokens: 0,
      }
      row[metric] = Number(point.value)
      byBucket.set(point.bucket, row)
    }
  }
  trend.value = [...byBucket.values()]
})
</script>

<template>
  <!-- Query the meter service yourself -->
  <MeterUsage
    title="Usage"
    :class-names="classNames"
  />
  <MeterTable
    title="Usage events"
    :columns="columns"
    :load="loadUsageRows"
  />
  <MeterChart
    type="line"
    :data="trend"
    x="bucket"
    :series="[
      { key: 'input', label: 'Input tokens', value: 'input_tokens' },
      { key: 'output', label: 'Output tokens', value: 'output_tokens' },
    ]"
  />
</template>

provideMeterClient should be called in a page- or app-level setup. All descendant Meter components reuse the same SubjectClient, so JWT requests are not duplicated per component.

Shared Filter Bar

MeterUsage, MeterChart and MeterTable can all be driven by one shared filter bar. The core package provides the state model, and the React/Vue packages render it:

  • createMeterFilterController(definitions, initialValues?) — framework-agnostic filter state with a subscribe API, just like the table controller.
  • MeterFilterBar — the standalone UI. Bind it with controller, or use values + onChange when the page owns the state.
  • useMeterFilters(controller) (React) / useMeterFilterValues(controller) (Vue) — subscribe page code to the values.

Filter keys map straight onto usage query dimensions, so the same value set works server-side and client-side:

import { useMemo } from 'react'
import { MeterChart, MeterFilterBar, MeterTable, MeterUsage, useMeterFilters } from '@meterry/component-react'
import { createMeterFilterController, mergeMeterFilters, type MeterFilterDefinition } from '@meterry/component'

const definitions: MeterFilterDefinition[] = [
  {
    key: 'model',
    label: 'Model',
    type: 'select',
    options: [{ label: 'GLM', value: 'glm-5.1' }, { label: 'GPT', value: 'gpt-4o' }],
  },
  { key: 'metric', label: 'Metric', type: 'text' },
]

function DashboardPage() {
  const controller = useMemo(() => createMeterFilterController(definitions), [])
  const filters = useMeterFilters(controller)

  return (
    <>
      <MeterFilterBar controller={controller} />

      {/* Server-side: filters ride on the usage query (UsageWidgetQuery.filters). */}
      <MeterUsage query={{ filters }} />

      {/* Client-side: filter the rows before handing them to the chart. */}
      <MeterChart type="bar" data={rowsFor(filters)} x="model" series={[…]} />

      {/* Table: pass filtered data, or forward the values to the service
          through load with mergeMeterFilters(query.filters, filters). */}
      <MeterTable columns={columns} data={rowsFor(filters)} />
    </>
  )
}

In Vue 3 the same pieces are MeterFilterBar (bind controller, or values + @change), useMeterFilterValues(controller), and <MeterUsage :query="{ filters }" />. mergeMeterFilters(...sources) combines several filter records (later sources win, blanks deactivate a key) — handy for merging the shared values with a table query's own column filters inside load.

The table's built-in column filters and the shared bar are independent: keep column filters for table-local concerns (the toolbar slot receives { filters, controller } if you want to customize them) and use the shared bar for dashboard-wide dimensions.

Core API

The core package has no React or Vue dependency:

import {
  createSubjectTokenProvider,
  SubjectClient,
  createChartModel,
  createTableController,
} from '@meterry/component'

SubjectClient handles Subject Token caching, expiry refresh, 401 retries, and the endpoints below. Besides passing getJwt directly, you can also create and reuse a SubjectTokenProvider explicitly:

const tokenProvider = createSubjectTokenProvider(getSubjectToken)
const client = new SubjectClient(tokenProvider, {
  baseUrl: 'https://api.meterry.com',
  credentials: 'include',
})

A single SubjectTokenProvider keeps only one JWT request in flight; other components or clients await the same Promise. 401 refreshes use the same single-flight logic, and each API request retries at most once.

By default, the provider persists Subject Tokens that carry expires_in or expires_at to the browser's localStorage, under the default key meter:subject-token. You can override the key, or disable persistence entirely:

const tokenProvider = createSubjectTokenProvider(getSubjectToken, {
  tokenStorageKey: `meter:${tenantId}:${userId}`,
  // tokenStorage: null, // in-memory cache only
})

Multi-user, multi-tenant, or multi-environment apps should give each authentication scope its own tokenStorageKey. In tokenScope compatibility mode, when no explicit tokenStorageKey is passed, the library automatically uses a scope-aware default key. On logout or account switch, call clearToken() or clearSharedSubjectToken(scopeKey).

  • POST /v1/subject/usage/query
  • POST /v1/subject/analysis/query
  • GET /v1/subject/usage/events/details
  • GET /v1/subject/realtime/balance

Beyond these wrapper methods, client.request(path, init) can call any endpoint directly (including endpoints added in the future); token injection, 401 refresh, and retry logic all apply:

const result = await client.request('/v1/subject/usage/export', {
  method: 'POST',
  body: JSON.stringify({ format: 'csv' }),
})

Extension Points

  • Custom chart types: ChartType is an open string union. Register SVG renderers (for headless / no-ECharts setups) with registerChartSvgRenderer(type, renderer), and ECharts series mappings with registerEChartsSeries(type, builder, { polar }); unregistered types fall back to the built-in line behavior.

  • Registry lifecycle and scoping: unregisterChartSvgRenderer(type) / unregisterEChartsSeries(type) remove a registration, restoring the line fallback for that type. When several independently deployed bundles coexist on one page (micro frontends), create isolated scoped registries with createChartSvgRendererRegistry() / createEChartsSeriesRegistry() — they never affect the global registry, and passing false creates an empty registry without the built-ins. renderChartSvg(model, registry) and createEChartsOption(model, registry) accept an explicit registry, and the React/Vue MeterChart / MeterUsage components support a seriesRegistry prop:

    import { createEChartsSeriesRegistry } from '@meterry/component'
    
    const registry = createEChartsSeriesRegistry()
    registry.register('r9s:spark', sparkBuilder)
    // Only affects components that receive this registry; the global one stays untouched
    <MeterChart type="r9s:spark" seriesRegistry={registry} ... />
  • On-demand ECharts module loading: the React/Vue packages bundle only the echarts modules needed by line/bar/pie. If a custom type needs another series (scatter, radar, etc.), register the required modules together with the series via the third argument of the framework packages' registerEChartsSeries, otherwise echarts cannot render it:

    import { ScatterChart } from 'echarts/charts'
    import { registerEChartsSeries } from '@meterry/component-react'
    
    registerEChartsSeries(
      'scatter',
      (series) => ({
        type: 'scatter',
        data: series.points.map((point) => point.value),
      }),
      { modules: [ScatterChart] },
    )
  • Deep UI customization: MeterUsage, MeterChart, and MeterTable in the React/Vue packages all support slots render props, letting you replace the rendering of the header / value / chart / toolbar / cell / pagination regions while keeping the component's internal data fetching and state management.

  • Custom filter semantics: a table column's filter.match accepts a (value, filterValue) => boolean function, enabling arbitrary matching logic such as date ranges or multi-select.

  • Internationalization / custom copy: every default string rendered by the components (titles, loading and empty states, pagination, filter placeholders, etc.) is centralized in MeterMessages. Override parts of it through the messages prop to localize; explicitly passed loading / empty / title props still take precedence:

    import type { MeterMessages } from '@meterry/component'
    
    const zhMessages: Partial<MeterMessages> = {
      caption: '计量',
      tableTitle: '用量事件',
      tableEmpty: '暂无数据。',
      previousPage: '上一页',
      nextPage: '下一页',
      paginationSummary: ({ total, page, pageCount }) =>
        `共 ${total} 条 · 第 ${page} / ${pageCount} 页`,
    }
    
    <MeterTable messages={zhMessages} columns={columns} load={loadRows} />

data updates on the table component flow through the internal setData: passing a new array reference from the parent only refreshes the data; filter, sort, and page state are never reset.

The browser should never hold a tenant API Key. getJwt should call your application backend, which issues a short-lived Subject Token based on the login session.

MeterChart and MeterUsage in the React/Vue packages use Apache ECharts for the actual drawing:

  • React uses echarts-for-react
  • Vue 3 uses vue-echarts
  • classNames.chart still controls the chart container's size, spacing, and appearance via Tailwind
  • Existing component APIs such as type, series, and theme are unchanged

Examples

examples/ contains three kinds of examples:

| Example | Framework | Description | | --- | --- | --- | | examples/react-showcase | React | A complete catalog of component usage: Provider, MeterUsage (query/slots/messages/compat mode), all five MeterChart types and themes, MeterTable (service data + local controller / remote load / controlled controller), a shared MeterFilterBar driving Usage/Chart/Table together, slots for every region, and custom chart types with registries. All data is fetched by the client from an in-browser mock of the meter service — no backend needed. | | examples/vue-showcase | Vue 3 | The Vue counterpart of the React showcase: provideMeterClient plus the same Usage/Chart/Table/shared-filter-bar/slots/custom-type usage. Also works without a backend. | | examples/new-r9s | React | A real Meter integration example: issues Subject Tokens with the new-r9s Meter configuration, queries Analysis / Usage / Usage Details across a time range, and ships a Node end-to-end test. |

The showcase examples are pure front-end examples (the meter service is mocked in src/mock-meter.ts, and all sample data is fetched from it through the client; when connecting to a real environment, switch back to the default fetch and configure baseUrl). To verify the real end-to-end path, use examples/new-r9s and its E2E test:

npm run dev:example:react-showcase   # React showcase dev server
npm run dev:example:vue-showcase     # Vue showcase dev server
npm run example:new-r9s              # real/mock Meter integration example
npm run example:new-r9s:e2e          # r9s Meter end-to-end test (mocked by default)

Documentation Site

The repository ships an interactive documentation site that runs on top of an in-browser mock — no backend required:

Docs site (VitePress) — complete guides + API reference, with interactive Vue live demos (MeterUsage / MeterChart / MeterTable) embedded in the pages:

npm run docs:dev        # dev server
npm run docs:build      # build to docs/.vitepress/dist
npm run docs:preview    # preview the build locally

Content lives in docs/: guide/ (quick start, SubjectClient auth, themes and styling, internationalization, chart extensions, and per-component pages) and api/ (export listings for core / React / Vue / Tailwind CSS). Live demo components live in docs/demos/ and reference the src and packages/vue sources directly through vite aliases.

Release

One-time setup

  1. npm token: create an Automation access token on npmjs.com (it bypasses 2FA) and add it as the NPM_TOKEN secret of the GitHub repository (Settings → Secrets and variables → Actions).
  2. GitHub Pages: to enable it, set Settings → Pages → Build and deployment → Source to GitHub Actions.

Routine release

npm run release -- patch   # or minor / major / an exact version like 1.2.3

The script runs, in order: verify a clean git working tree → build all packages → type-check + tests → sync the version and peerDependencies across the 4 packages (core / react / vue / tailwindcss) → git commit + create the v<version> tag and push it. When the tag is pushed, GitHub Actions (.github/workflows/release.yml) publishes the 4 packages to npm (the private example workspaces are never published) and creates the GitHub Release.

Common flags:

| Flag | Description | | --- | --- | | --publish-local | Skip CI and publish the 4 packages locally from the release script (requires npm login) | | --dist-tag next | Set the npm dist-tag; prerelease versions (e.g. 2.0.0-beta.1) default to next automatically | | --dry-run | Print the plan only; change nothing | | --skip-tests | Skip type checking and tests | | --no-git / --force | Skip the git commit/tag / allow a dirty working tree |

CI workflows

| Workflow | Trigger | Purpose | | --- | --- | --- | | ci.yml | push / PR | Build, type-check, and test on a Node 22 + 24 matrix | | release.yml | v* tag push / manual | Publish the 4 packages to npm (provenance attached automatically for public repos) and create the GitHub Release |

Common causes of a failed release: NPM_TOKEN is missing or expired (look for ENEEDAUTH in the logs); the version already exists on npm (the local script pre-checks this, and npm on the CI side rejects duplicates as a backstop); the repository is private (provenance is skipped automatically — publishing still succeeds).

Local Development

npm install
npm test
npm run typecheck
npm run build:packages

Real Meter integration example:

METER_EXAMPLE_MOCK=true npm run example:new-r9s

The examples do not register custom elements: React/Vue charts are rendered by the mature ECharts components, and tables keep using the shared headless controller.