@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-reactVue 3:
npm install @meterry/component @meterry/component-vue echarts vue-echartsIf you use Tailwind CSS:
npm install @meterry/component-tailwindcssAutomatic 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 followsafter_id/next_after_idas needed, reuses cached batches, handles unknown totals, cancellation, retry and refresh.source="usage"(default): queries aggregate rows, generates columns fromquery.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.
MeterTableremains available for advanceddata/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 asubscribeAPI, just like the table controller.MeterFilterBar— the standalone UI. Bind it withcontroller, or usevalues+onChangewhen 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/queryPOST /v1/subject/analysis/queryGET /v1/subject/usage/events/detailsGET /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:
ChartTypeis an open string union. Register SVG renderers (for headless / no-ECharts setups) withregisterChartSvgRenderer(type, renderer), and ECharts series mappings withregisterEChartsSeries(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 withcreateChartSvgRendererRegistry()/createEChartsSeriesRegistry()— they never affect the global registry, and passingfalsecreates an empty registry without the built-ins.renderChartSvg(model, registry)andcreateEChartsOption(model, registry)accept an explicit registry, and the React/VueMeterChart/MeterUsagecomponents support aseriesRegistryprop: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, andMeterTablein the React/Vue packages all supportslotsrender 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.matchaccepts a(value, filterValue) => booleanfunction, 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 themessagesprop to localize; explicitly passedloading/empty/titleprops 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.chartstill controls the chart container's size, spacing, and appearance via Tailwind- Existing component APIs such as
type,series, andthemeare 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 locallyContent 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
- npm token: create an Automation access token on npmjs.com (it bypasses 2FA) and add it as the
NPM_TOKENsecret of the GitHub repository (Settings → Secrets and variables → Actions). - 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.3The 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:packagesReal Meter integration example:
METER_EXAMPLE_MOCK=true npm run example:new-r9sThe examples do not register custom elements: React/Vue charts are rendered by the mature ECharts components, and tables keep using the shared headless controller.
