@r0hitsharma/dashboard-kit 0.12.0-rohit-fork-ci.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # @r0hitsharma/dashboard-kit
2
+
3
+ A generic, declarative dashboard engine. You describe a dashboard as data — a
4
+ `DashboardSpec` manifest — and the engine renders it: a recursive layout tree of
5
+ resizable splits and widgets, a flat widget registry, typed data bindings, and
6
+ declared cross-widget interaction. It ships only the ENGINE and a small set of
7
+ GENERIC adapters over `@r0hitsharma/design-system` and
8
+ `@r0hitsharma/charting`; every domain-specific widget, data source, and
9
+ component binding is the consumer's to register.
10
+
11
+ ## Install
12
+
13
+ ```jsonc
14
+ // peerDependencies
15
+ "@r0hitsharma/dashboard-kit": "*",
16
+ "@r0hitsharma/design-system": "*",
17
+ "@r0hitsharma/charting": "*",
18
+ "react": "^19",
19
+ "react-dom": "^19"
20
+ ```
21
+
22
+ ## Quick start
23
+
24
+ ```tsx
25
+ import {
26
+ DashboardRenderer,
27
+ type DashboardSpec,
28
+ type DashboardDataSources,
29
+ } from '@r0hitsharma/dashboard-kit';
30
+
31
+ const spec: DashboardSpec = {
32
+ version: 1,
33
+ title: 'Overview',
34
+ layout: {
35
+ type: 'split',
36
+ direction: 'column',
37
+ children: [
38
+ { type: 'widget', ref: 'kpis' },
39
+ {
40
+ type: 'split',
41
+ direction: 'row',
42
+ children: [
43
+ { type: 'widget', ref: 'trend', size: 2 },
44
+ { type: 'widget', ref: 'table', size: 1 },
45
+ ],
46
+ },
47
+ ],
48
+ },
49
+ widgets: {
50
+ kpis: {
51
+ id: 'kpis',
52
+ component: 'statRow',
53
+ title: 'Key metrics',
54
+ dataBinding: { source: 'metrics', fields: { label: 'name', value: 'value' } },
55
+ interaction: { reads: ['highlightedKey'] },
56
+ },
57
+ trend: {
58
+ id: 'trend',
59
+ component: 'lineChart',
60
+ title: 'Trend',
61
+ dataBinding: { source: 'series', fields: {} },
62
+ props: { xField: 'x', series: [{ key: 'A', field: 'a' }] },
63
+ },
64
+ table: {
65
+ id: 'table',
66
+ component: 'table',
67
+ title: 'Rows',
68
+ dataBinding: { source: 'metrics', fields: { rowKey: 'name' } },
69
+ props: {
70
+ columns: [
71
+ { accessorKey: 'name', header: 'Name' },
72
+ { accessorKey: 'value', header: 'Value', render: 'number' },
73
+ ],
74
+ },
75
+ interaction: { writes: ['highlightedKey'] },
76
+ },
77
+ },
78
+ };
79
+
80
+ const dataSources: DashboardDataSources = {
81
+ metrics: [
82
+ { name: 'Alpha', value: 42 },
83
+ { name: 'Beta', value: 17 },
84
+ ],
85
+ series: [
86
+ { x: 'Mon', a: 3 },
87
+ { x: 'Tue', a: 5 },
88
+ ],
89
+ };
90
+
91
+ export function Example() {
92
+ return <DashboardRenderer spec={spec} dataSources={dataSources} />;
93
+ }
94
+ ```
95
+
96
+ Clicking a `table` row writes `highlightedKey`; the `statRow` reads it and
97
+ emphasizes the matching tile — cross-highlighting driven entirely by the
98
+ manifest's declared `interaction`.
99
+
100
+ ## The manifest
101
+
102
+ - `layout` — a recursive tree of `split` (row/column, optionally `resizable`)
103
+ and `widget` (a `ref` into `widgets`) nodes. Positioning only; independent of
104
+ what each leaf renders.
105
+ - `widgets` — a flat, id-keyed map of widget nodes: `{ component, props,
106
+ dataBinding, interaction, thresholds }`.
107
+ - `dataBinding` — `{ source, fields }`: a data-source id plus a channel→field
108
+ mapping (Vega-Lite style, not a query string).
109
+ - `interaction` — `reads` / `writes` shared-selection keys, plus `agentWritable`
110
+ (default-deny agent exposure; see below).
111
+
112
+ ## Extending the registry
113
+
114
+ Register your own component keys → adapters and merge them over the defaults:
115
+
116
+ ```tsx
117
+ import { DashboardRenderer, mergeRegistries, type RegistryComponent } from '@r0hitsharma/dashboard-kit';
118
+
119
+ const Gauge: RegistryComponent = ({ widget, data }) => /* ... */;
120
+ const registry = mergeRegistries({ gauge: Gauge });
121
+
122
+ <DashboardRenderer spec={spec} dataSources={ds} registry={registry} />;
123
+ ```
124
+
125
+ The default registry provides `note`, `stat`, `statRow`, `table`, `lineChart`,
126
+ and `areaChart`.
127
+
128
+ ## Validation
129
+
130
+ `validateDashboardSpec(candidate, { knownComponents })` runs a zod structural
131
+ pass plus referential passes (unknown refs, orphaned widgets, a widget `id` that
132
+ disagrees with its registry key, resizable column-split without a height, an
133
+ `agentWritable` key that isn't writable, optional unknown-component check). It
134
+ never throws — an agent-submitted patch is untrusted input, so it returns
135
+ `{ ok, issues }` and the renderer shows an annotated rejection rather than
136
+ blanking. `DashboardRenderer` runs it by default;
137
+ pass `skipValidation` for a spec you author and trust.
138
+
139
+ `collectAgentWritableKeys(spec)` is the other half of the `agentWritable`
140
+ contract: it returns the set of keys the manifest actually permits an agent to
141
+ write, so a host app's agent-facing tools can refuse anything outside it.
142
+
143
+ ## Interaction
144
+
145
+ By default the renderer uses a self-contained local interaction store, enough
146
+ for a standalone dashboard. To cross-filter a synced chart group, back
147
+ interaction with charting's real per-key store via `useChartingInteraction`
148
+ (see `DESIGN.md`).
149
+
150
+ See `DESIGN.md` for the full contract and design rationale.
@@ -0,0 +1,47 @@
1
+ import { type InteractionKey } from '@r0hitsharma/charting';
2
+ import { type ReactElement } from 'react';
3
+ import type { InteractionContextValue } from './interaction.js';
4
+ /**
5
+ * Adapts a manifest's free-form interaction vocabulary onto charting's REAL
6
+ * per-key store (`DashboardInteractionProvider` / `useDashboardInteraction` /
7
+ * `useInteractionValue`), so a declarative dashboard cross-filters the same
8
+ * synced chart group a hand-built one would.
9
+ *
10
+ * The reconciliation this file owns: a manifest names keys in an FDC3-shaped
11
+ * vocabulary (`highlightedAsset`, `selectedTimeRange`) while charting's store
12
+ * is typed to `highlightedKey` / `timeRange` / `hoveredTimestamp`. The key map
13
+ * below is the single, declared place that translation happens — not a rename
14
+ * of either side. Consumers can pass their own map to extend the vocabulary.
15
+ *
16
+ * ── Why a bridge rather than calling `useDashboardInteraction` in each widget
17
+ * Charting's `DashboardInteractionContext` value changes identity on every
18
+ * pointer move (`hoveredTimestamp` lives in it), so anything that consumes it
19
+ * re-renders at hover frequency. {@link InteractionSync} is the SOLE consumer:
20
+ * a null-rendering component that republishes the mapped keys into a tiny
21
+ * per-key store. A widget that declared `interaction.reads: ['highlightedAsset']`
22
+ * then re-renders when the highlight changes and at no other time — matching
23
+ * the discipline of charting's own `useInteractionValue`.
24
+ */
25
+ export type InteractionKeyMap = Record<string, InteractionKey>;
26
+ /**
27
+ * Default manifest-key -> charting-key aliases. Native charting keys map to
28
+ * themselves so a manifest may also name them directly.
29
+ */
30
+ export declare const DEFAULT_INTERACTION_KEY_MAP: InteractionKeyMap;
31
+ export type ChartingInteraction = {
32
+ /** The generic surface to hand the renderer's `interaction` prop. */
33
+ interaction: InteractionContextValue;
34
+ /**
35
+ * Render ONCE inside a `SyncedChartGroup` / `DashboardInteractionProvider`.
36
+ * It renders nothing observable; it is the sole subscriber to the
37
+ * high-frequency charting context and republishes the mapped keys into the
38
+ * per-key store that {@link interaction} reads.
39
+ */
40
+ InteractionSync: () => ReactElement;
41
+ };
42
+ /**
43
+ * Builds a charting-backed {@link InteractionContextValue}. Call it above a
44
+ * `SyncedChartGroup`, hand `interaction` to `DashboardRenderer`, and render
45
+ * `<InteractionSync />` inside the group.
46
+ */
47
+ export declare function useChartingInteraction(keyMap?: InteractionKeyMap): ChartingInteraction;
@@ -0,0 +1,112 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useDashboardInteraction, } from '@r0hitsharma/charting';
3
+ import { useCallback, useEffect, useMemo, useRef, } from 'react';
4
+ /**
5
+ * Default manifest-key -> charting-key aliases. Native charting keys map to
6
+ * themselves so a manifest may also name them directly.
7
+ */
8
+ export const DEFAULT_INTERACTION_KEY_MAP = {
9
+ highlightedAsset: 'highlightedKey',
10
+ highlightedKey: 'highlightedKey',
11
+ selectedTimeRange: 'timeRange',
12
+ timeRange: 'timeRange',
13
+ hoveredTimestamp: 'hoveredTimestamp',
14
+ };
15
+ /**
16
+ * Module-scope, not a closure created per `useChartingInteraction` call:
17
+ * hooks nested inside a `useMemo`/`useCallback` are ambiguous to the rules-of
18
+ * -hooks check (it can't tell they belong to a separate component render, not
19
+ * the enclosing hook's), so this needs to be a real, statically top-level
20
+ * component. Everything it needs comes in as props instead of a closure.
21
+ *
22
+ * `keyMap` is a plain prop, not a ref synced from a parent effect: React
23
+ * flushes child effects before parent effects, so a ref updated by
24
+ * `useChartingInteraction`'s own effect would still hold the PREVIOUS
25
+ * `keyMap` when this component's effects below run in the commit where it
26
+ * actually changed. A prop is simply current already.
27
+ */
28
+ function ChartingInteractionSync({ keyMap, writerRef, publish, }) {
29
+ const chart = useDashboardInteraction();
30
+ // Every manifest alias that resolves to `highlightedKey` gets the
31
+ // charting value republished under it; likewise for the other keys.
32
+ const aliasesFor = useCallback((chartingKey) => Object.entries(keyMap)
33
+ .filter(([, target]) => target === chartingKey)
34
+ .map(([alias]) => alias), [keyMap]);
35
+ useEffect(() => {
36
+ for (const alias of aliasesFor('highlightedKey')) {
37
+ publish(alias, chart.highlightedKey ?? undefined);
38
+ }
39
+ }, [chart.highlightedKey, publish, aliasesFor]);
40
+ useEffect(() => {
41
+ for (const alias of aliasesFor('timeRange')) {
42
+ publish(alias, chart.timeRange ?? undefined);
43
+ }
44
+ }, [chart.timeRange, publish, aliasesFor]);
45
+ useEffect(() => {
46
+ for (const alias of aliasesFor('hoveredTimestamp')) {
47
+ publish(alias, chart.hoveredTimestamp ?? undefined);
48
+ }
49
+ }, [chart.hoveredTimestamp, publish, aliasesFor]);
50
+ // The write side, held in a ref so `interaction.write` stays
51
+ // identity-stable for every consumer.
52
+ useEffect(() => {
53
+ writerRef.current = (key, value) => {
54
+ switch (keyMap[key]) {
55
+ case 'highlightedKey':
56
+ chart.setHighlightedKey(value == null ? null : String(value));
57
+ return;
58
+ case 'timeRange':
59
+ chart.setTimeRange(value ?? null);
60
+ return;
61
+ case 'hoveredTimestamp':
62
+ chart.setHoveredTimestamp(value == null ? null : Number(value));
63
+ return;
64
+ default:
65
+ return;
66
+ }
67
+ };
68
+ }, [chart, keyMap, writerRef]);
69
+ return null;
70
+ }
71
+ /**
72
+ * Builds a charting-backed {@link InteractionContextValue}. Call it above a
73
+ * `SyncedChartGroup`, hand `interaction` to `DashboardRenderer`, and render
74
+ * `<InteractionSync />` inside the group.
75
+ */
76
+ export function useChartingInteraction(keyMap = DEFAULT_INTERACTION_KEY_MAP) {
77
+ const valuesRef = useRef(new Map());
78
+ const listenersRef = useRef(new Map());
79
+ const writerRef = useRef(() => { });
80
+ const read = useCallback((key) => valuesRef.current.get(key), []);
81
+ const write = useCallback((key, value) => {
82
+ writerRef.current(key, value);
83
+ }, []);
84
+ const subscribe = useCallback((key, onChange) => {
85
+ let listeners = listenersRef.current.get(key);
86
+ if (!listeners) {
87
+ listeners = new Set();
88
+ listenersRef.current.set(key, listeners);
89
+ }
90
+ listeners.add(onChange);
91
+ return () => {
92
+ listeners.delete(onChange);
93
+ };
94
+ }, []);
95
+ const publish = useCallback((key, value) => {
96
+ if (Object.is(valuesRef.current.get(key), value))
97
+ return;
98
+ valuesRef.current.set(key, value);
99
+ for (const listener of listenersRef.current.get(key) ?? [])
100
+ listener();
101
+ }, []);
102
+ const interaction = useMemo(() => ({ read, write, subscribe }), [read, write, subscribe]);
103
+ // A stable function identity, so React doesn't unmount/remount
104
+ // `ChartingInteractionSync` (and its charting subscription) across the
105
+ // dashboard's renders — as long as `keyMap` doesn't churn identity either.
106
+ // Pass a memoized `keyMap` (module-scoped, like the default, or your own
107
+ // `useMemo`); an inline object here remounts the subscription every render,
108
+ // the same discipline `usePlayback`'s `source` and `useDataTable`'s
109
+ // `columns` already require of their callers.
110
+ const InteractionSync = useCallback(() => (_jsx(ChartingInteractionSync, { keyMap: keyMap, writerRef: writerRef, publish: publish })), [publish, keyMap]);
111
+ return { interaction, InteractionSync };
112
+ }
@@ -0,0 +1,6 @@
1
+ export type { DashboardSpec, DashboardDataSources, DataBinding, LayoutDirection, LayoutNode, SplitLayoutNode, ThresholdRule, ThresholdSeverity, WidgetInteraction, WidgetLayoutNode, WidgetNode, WidgetTableColumn, } from './schema.js';
2
+ export { DashboardRenderer, renderLayoutNode, type DashboardRendererProps, } from './renderer.js';
3
+ export { DEFAULT_REGISTRY, UnknownWidget, mergeRegistries, type ComponentRegistry, type RegistryComponent, type RegistryComponentProps, } from './registry.js';
4
+ export { collectAgentWritableKeys, dashboardSpecSchema, validateDashboardSpec, type ManifestIssue, type ManifestValidation, type ValidateOptions, } from './validate.js';
5
+ export { useInteractionField, useLocalInteraction, type InteractionContextValue, } from './interaction.js';
6
+ export { DEFAULT_INTERACTION_KEY_MAP, useChartingInteraction, type ChartingInteraction, type InteractionKeyMap, } from './charting-interaction.js';
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ // The recursive renderer.
2
+ export { DashboardRenderer, renderLayoutNode, } from './renderer.js';
3
+ // The string -> component registry mechanism + the default generic adapters.
4
+ export { DEFAULT_REGISTRY, UnknownWidget, mergeRegistries, } from './registry.js';
5
+ // Structural + referential manifest validation, and the agent-exposure query.
6
+ export { collectAgentWritableKeys, dashboardSpecSchema, validateDashboardSpec, } from './validate.js';
7
+ // The generic interaction surface: the local store + the per-key read hook.
8
+ export { useInteractionField, useLocalInteraction, } from './interaction.js';
9
+ // The adapter onto charting's real per-key interaction store.
10
+ export { DEFAULT_INTERACTION_KEY_MAP, useChartingInteraction, } from './charting-interaction.js';
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The interaction surface a manifest's widgets read and write, addressed by
3
+ * the free-form string keys a `WidgetNode.interaction.reads`/`writes` names.
4
+ *
5
+ * This is deliberately a small, transport-agnostic `{read, write, subscribe}`
6
+ * contract rather than charting's concrete `DashboardInteractionApi` (which is
7
+ * typed to a fixed set of keys: `timeRange`, `hoveredTimestamp`, `filters`,
8
+ * `highlightedKey`). A manifest names its OWN vocabulary (`highlightedAsset`,
9
+ * `selectedTimeRange`, ...); {@link module:charting-interaction} is the adapter
10
+ * that maps that vocabulary onto charting's real per-key store. Keeping the
11
+ * engine's contract generic is what lets a consumer back interaction with
12
+ * charting, with their own store, or with the built-in local store below —
13
+ * without the engine caring which.
14
+ */
15
+ export type InteractionContextValue = {
16
+ read: (key: string) => unknown;
17
+ write: (key: string, value: unknown) => void;
18
+ /**
19
+ * Optional per-key change subscription. When present, widgets reach values
20
+ * through {@link useInteractionField} rather than calling `read` during
21
+ * render, so only the widgets bound to a changed key re-render — the same
22
+ * hover-perf discipline charting's own per-key store enforces.
23
+ */
24
+ subscribe?: (key: string, onChange: () => void) => () => void;
25
+ };
26
+ /**
27
+ * A self-contained interaction store backed by React state — the default the
28
+ * renderer falls back to when no `interaction` prop is supplied. Correct and
29
+ * cheap for a standalone manifest (a story, a preview) that isn't synced to a
30
+ * live chart group; for chart-synced interaction, pass the adapter from
31
+ * {@link module:charting-interaction} instead.
32
+ */
33
+ export declare function useLocalInteraction(): InteractionContextValue;
34
+ /**
35
+ * Reads ONE interaction key, subscribing to just that key when the provider
36
+ * supports it. `key` may be `undefined` (a widget that declares no
37
+ * `interaction.reads`), in which case this is a no-op returning `undefined`.
38
+ *
39
+ * Named `useInteractionField` (not `useInteractionValue`) so a consumer file
40
+ * can import both this and charting's own `useInteractionValue` without a name
41
+ * collision.
42
+ */
43
+ export declare function useInteractionField(interaction: InteractionContextValue, key: string | undefined): unknown;
@@ -0,0 +1,41 @@
1
+ import { useCallback, useMemo, useRef, useState, useSyncExternalStore, } from 'react';
2
+ /**
3
+ * A self-contained interaction store backed by React state — the default the
4
+ * renderer falls back to when no `interaction` prop is supplied. Correct and
5
+ * cheap for a standalone manifest (a story, a preview) that isn't synced to a
6
+ * live chart group; for chart-synced interaction, pass the adapter from
7
+ * {@link module:charting-interaction} instead.
8
+ */
9
+ export function useLocalInteraction() {
10
+ const [, setState] = useState({});
11
+ // Keep `read`/`write` identity-stable so the context value doesn't change
12
+ // for reasons other than a real state change.
13
+ const stateRef = useRef({});
14
+ const read = useCallback((key) => stateRef.current[key], []);
15
+ const write = useCallback((key, value) => {
16
+ if (Object.is(stateRef.current[key], value))
17
+ return;
18
+ stateRef.current = { ...stateRef.current, [key]: value };
19
+ setState(stateRef.current);
20
+ }, []);
21
+ return useMemo(() => ({ read, write }), [read, write]);
22
+ }
23
+ /**
24
+ * Reads ONE interaction key, subscribing to just that key when the provider
25
+ * supports it. `key` may be `undefined` (a widget that declares no
26
+ * `interaction.reads`), in which case this is a no-op returning `undefined`.
27
+ *
28
+ * Named `useInteractionField` (not `useInteractionValue`) so a consumer file
29
+ * can import both this and charting's own `useInteractionValue` without a name
30
+ * collision.
31
+ */
32
+ export function useInteractionField(interaction, key) {
33
+ const { read, subscribe } = interaction;
34
+ const subscribeToKey = useCallback((onChange) => {
35
+ if (!key || !subscribe)
36
+ return () => { };
37
+ return subscribe(key, onChange);
38
+ }, [key, subscribe]);
39
+ const getSnapshot = useCallback(() => (key ? read(key) : undefined), [key, read]);
40
+ return useSyncExternalStore(subscribeToKey, getSnapshot, getSnapshot);
41
+ }
@@ -0,0 +1,37 @@
1
+ import { type JSX } from 'react';
2
+ import { type InteractionContextValue } from './interaction.js';
3
+ import type { WidgetNode } from './schema.js';
4
+ /**
5
+ * The string -> component registry. A manifest's `WidgetNode.component` names a
6
+ * key here; the resolved adapter owns translating that widget's
7
+ * `dataBinding.fields` mapping + `props` into a real component's prop shape, so
8
+ * the manifest stays declarative (field names, not JSX).
9
+ *
10
+ * The registry is an EXTENSIBLE MECHANISM: {@link DEFAULT_REGISTRY} ships a
11
+ * small set of generic adapters over public design-system + charting
12
+ * components, and a consumer merges in their own domain adapters with
13
+ * {@link mergeRegistries} (or replaces it wholesale). The engine itself knows
14
+ * nothing about any specific widget.
15
+ */
16
+ export type RegistryComponentProps = {
17
+ widget: WidgetNode;
18
+ data: Record<string, unknown>[];
19
+ interaction: InteractionContextValue;
20
+ };
21
+ export type RegistryComponent = (props: RegistryComponentProps) => JSX.Element;
22
+ export type ComponentRegistry = Record<string, RegistryComponent>;
23
+ /**
24
+ * The default registry: a small set of GENERIC adapters over public
25
+ * design-system + charting components. Nothing domain-specific — a consumer
26
+ * merges in their own component keys with {@link mergeRegistries}.
27
+ */
28
+ export declare const DEFAULT_REGISTRY: ComponentRegistry;
29
+ /** Merges consumer adapters over the defaults (consumer keys win on collision). */
30
+ export declare function mergeRegistries(...registries: ComponentRegistry[]): ComponentRegistry;
31
+ /**
32
+ * Inline marker rendered in place of a widget whose ref or component key does
33
+ * not resolve — one bad manifest entry never blanks the whole dashboard.
34
+ */
35
+ export declare function UnknownWidget({ label }: {
36
+ label: string;
37
+ }): JSX.Element;
@@ -0,0 +1,220 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Axis, AreaSeries, Grid, LineSeries, Tooltip, XYChart, chartTheme, } from '@r0hitsharma/charting';
3
+ import { Badge, DataTable, Sparkline, StatRow, StatTile, useDataTable, } from '@r0hitsharma/design-system';
4
+ import { useCallback, useRef, useState, } from 'react';
5
+ import { useInteractionField, } from './interaction.js';
6
+ const lastRecord = (data) => (data.length > 0 ? data[data.length - 1] : {});
7
+ const toNumber = (value) => typeof value === 'number' ? value : Number(value ?? Number.NaN);
8
+ function resolveThresholdTone(value, thresholds) {
9
+ if (value === undefined || !thresholds)
10
+ return 'default';
11
+ for (const rule of thresholds) {
12
+ const hit = (rule.op === 'gte' && value >= rule.value) ||
13
+ (rule.op === 'lte' && value <= rule.value) ||
14
+ (rule.op === 'gt' && value > rule.value) ||
15
+ (rule.op === 'lt' && value < rule.value) ||
16
+ (rule.op === 'eq' && value === rule.value);
17
+ if (hit)
18
+ return rule.severity === 'success' ? 'success' : 'critical';
19
+ }
20
+ return 'default';
21
+ }
22
+ /** Fallback width used only before the first ResizeObserver measurement. */
23
+ const FALLBACK_CHART_WIDTH = 560;
24
+ /**
25
+ * Sizes a chart's SVG `width` (a hard pixel dimension, not CSS) to whatever
26
+ * width its container is granted, via a `ResizeObserver` on a callback ref. An
27
+ * explicit `widget.props.width` still wins.
28
+ */
29
+ function useChartWidth(explicitWidth) {
30
+ const [measured, setMeasured] = useState(FALLBACK_CHART_WIDTH);
31
+ const observerRef = useRef(null);
32
+ const ref = useCallback((el) => {
33
+ observerRef.current?.disconnect();
34
+ observerRef.current = null;
35
+ if (explicitWidth != null || !el)
36
+ return;
37
+ const observer = new ResizeObserver((entries) => {
38
+ const width = entries[0]?.contentRect.width;
39
+ if (width && width > 0)
40
+ setMeasured(Math.floor(width));
41
+ });
42
+ observer.observe(el);
43
+ observerRef.current = observer;
44
+ }, [explicitWidth]);
45
+ return { ref, width: explicitWidth ?? measured };
46
+ }
47
+ const CHART_MARGIN = { top: 16, right: 24, bottom: 32, left: 48 };
48
+ const noteStyle = {
49
+ color: 'var(--colors-text-muted)',
50
+ fontSize: '0.875rem',
51
+ lineHeight: 1.6,
52
+ margin: 0,
53
+ };
54
+ /** A free-form note paragraph, text supplied via `props.text`. */
55
+ const NoteWidget = ({ widget }) => (_jsx("p", { style: noteStyle, children: String(widget.props?.text ?? '') }));
56
+ /**
57
+ * Formats a numeric value per `props.format`. Non-numeric input falls through
58
+ * to its string form (or an em dash).
59
+ */
60
+ function formatStatValue(value, format) {
61
+ const numeric = toNumber(value);
62
+ if (!Number.isFinite(numeric))
63
+ return String(value ?? '—');
64
+ switch (format) {
65
+ case 'percent':
66
+ return `${(numeric * 100).toFixed(1)}%`;
67
+ case 'ratio':
68
+ return numeric.toFixed(2);
69
+ case 'currency':
70
+ return `$${numeric.toLocaleString('en-US', { maximumFractionDigits: 0 })}`;
71
+ default:
72
+ return String(numeric);
73
+ }
74
+ }
75
+ /** A single stat tile; value/tone resolved from the bound source + thresholds. */
76
+ const StatWidget = ({ widget, data }) => {
77
+ const fields = widget.dataBinding?.fields ?? {};
78
+ const record = lastRecord(data);
79
+ const rawValue = fields.value ? record[fields.value] : undefined;
80
+ const numeric = toNumber(rawValue);
81
+ const tone = resolveThresholdTone(Number.isFinite(numeric) ? numeric : undefined, widget.thresholds);
82
+ return (_jsx(StatTile, { label: widget.props?.label ?? widget.title ?? widget.id, value: formatStatValue(rawValue, widget.props?.format), sub: widget.props?.sub, tone: tone }));
83
+ };
84
+ /**
85
+ * A row of stat tiles, one per bound record. Reads its first declared
86
+ * interaction key (`interaction.reads[0]`) as a highlighted-row key: the tile
87
+ * whose label field matches gets a `success` tone, proving `reads` is wired.
88
+ */
89
+ const StatRowWidget = ({ widget, data, interaction }) => {
90
+ const fields = widget.dataBinding?.fields ?? {};
91
+ const highlightKey = widget.interaction?.reads?.[0];
92
+ const highlighted = useInteractionField(interaction, highlightKey);
93
+ return (_jsx(StatRow, { children: data.map((record, index) => {
94
+ const label = fields.label ? String(record[fields.label]) : `#${index}`;
95
+ const value = fields.value ? record[fields.value] : undefined;
96
+ const isHighlighted = highlighted != null && String(highlighted) === label;
97
+ return (_jsx(StatTile, { label: label, value: formatStatValue(value, widget.props?.format), sub: fields.sub ? String(record[fields.sub] ?? '') : undefined, tone: isHighlighted ? 'success' : 'default' }, label));
98
+ }) }));
99
+ };
100
+ function renderCell(column, value) {
101
+ switch (column.render) {
102
+ case 'number':
103
+ return typeof value === 'number'
104
+ ? value.toLocaleString('en-US', { maximumFractionDigits: 2 })
105
+ : String(value ?? '—');
106
+ case 'currency':
107
+ return typeof value === 'number'
108
+ ? `$${value.toLocaleString('en-US', { maximumFractionDigits: 0 })}`
109
+ : String(value ?? '—');
110
+ case 'percent':
111
+ return typeof value === 'number'
112
+ ? `${value.toFixed(1)}%`
113
+ : String(value ?? '—');
114
+ case 'badge': {
115
+ const palette = value === 'low' || value === 'success'
116
+ ? 'green'
117
+ : value === 'high' || value === 'critical'
118
+ ? 'red'
119
+ : 'amber';
120
+ return (_jsx(Badge, { colorPalette: palette, variant: "subtle", children: String(value ?? '—') }));
121
+ }
122
+ case 'sparkline':
123
+ return Array.isArray(value) ? (_jsx(Sparkline, { data: value, width: 96, height: 28, area: true })) : ('—');
124
+ default:
125
+ return String(value ?? '—');
126
+ }
127
+ }
128
+ /**
129
+ * A data table with declarative columns (`props.columns`). Writes its first
130
+ * declared interaction key (`interaction.writes[0]`) on row click — the
131
+ * clicked row's key value — proving `writes` is wired; the same key read by a
132
+ * sibling widget drives cross-highlighting.
133
+ */
134
+ const TableWidget = ({ widget, data, interaction }) => {
135
+ const specColumns = widget.props?.columns ?? [];
136
+ const rowKeyField = widget.dataBinding?.fields?.rowKey ?? specColumns[0]?.accessorKey;
137
+ const writeKey = widget.interaction?.writes?.[0];
138
+ const highlightKey = widget.interaction?.reads?.[0] ?? writeKey;
139
+ const selected = useInteractionField(interaction, highlightKey);
140
+ const columns = specColumns.map((column) => ({
141
+ accessorKey: column.accessorKey,
142
+ header: column.header,
143
+ cell: (info) => renderCell(column, info.getValue()),
144
+ }));
145
+ const table = useDataTable(data, columns);
146
+ const getRowKey = useCallback((row) => rowKeyField ? String(row[rowKeyField]) : '', [rowKeyField]);
147
+ return (_jsx(DataTable, { table: table, isLoading: false, getRowKey: getRowKey, selectedRowKey: selected != null ? String(selected) : undefined, onRowClick: writeKey && rowKeyField
148
+ ? (row) => interaction.write(writeKey, row[rowKeyField])
149
+ : undefined }));
150
+ };
151
+ const chartWrapStyle = {
152
+ width: '100%',
153
+ minWidth: 0,
154
+ overflowX: 'auto',
155
+ };
156
+ function readSeries(widget) {
157
+ const raw = widget.props?.series;
158
+ if (Array.isArray(raw))
159
+ return raw;
160
+ // Fall back to a single series bound through `dataBinding.fields.value`.
161
+ const field = widget.dataBinding?.fields?.value;
162
+ return field ? [{ key: widget.title ?? widget.id, field }] : [];
163
+ }
164
+ function makeChartWidget(kind) {
165
+ return function ChartWidget({ widget, data }) {
166
+ const explicitWidth = widget.props?.width;
167
+ const height = widget.props?.height ?? 260;
168
+ const { ref, width } = useChartWidth(explicitWidth);
169
+ const xField = widget.props?.xField ?? 'x';
170
+ const series = readSeries(widget);
171
+ const label = widget.props?.ariaLabel ??
172
+ widget.title ??
173
+ `${kind} chart`;
174
+ if (data.length === 0 || series.length === 0) {
175
+ return _jsx("div", { ref: ref, style: chartWrapStyle });
176
+ }
177
+ const xAccessor = (d) => d[xField];
178
+ return (_jsx("div", { ref: ref, style: chartWrapStyle, role: "img", "aria-label": label, children: _jsxs(XYChart, { theme: chartTheme, width: width, height: height, margin: CHART_MARGIN, xScale: { type: 'band', paddingInner: 0.3 }, yScale: { type: 'linear', nice: true }, children: [_jsx(Grid, { columns: false, numTicks: 4 }), _jsx(Axis, { orientation: "bottom", numTicks: 4 }), _jsx(Axis, { orientation: "left", numTicks: 4 }), series.map((s) => kind === 'line' ? (_jsx(LineSeries, { dataKey: s.key, data: data, xAccessor: xAccessor, yAccessor: (d) => toNumber(d[s.field]) }, s.key)) : (_jsx(AreaSeries, { dataKey: s.key, data: data, xAccessor: xAccessor, yAccessor: (d) => toNumber(d[s.field]), fillOpacity: 0.3 }, s.key))), _jsx(Tooltip, { snapTooltipToDatumX: true, snapTooltipToDatumY: true, showSeriesGlyphs: true, renderTooltip: ({ tooltipData }) => {
179
+ const datum = tooltipData?.nearestDatum?.datum;
180
+ return datum ? String(datum[xField]) : null;
181
+ } })] }) }));
182
+ };
183
+ }
184
+ /**
185
+ * The default registry: a small set of GENERIC adapters over public
186
+ * design-system + charting components. Nothing domain-specific — a consumer
187
+ * merges in their own component keys with {@link mergeRegistries}.
188
+ */
189
+ export const DEFAULT_REGISTRY = {
190
+ note: NoteWidget,
191
+ stat: StatWidget,
192
+ statRow: StatRowWidget,
193
+ table: TableWidget,
194
+ lineChart: makeChartWidget('line'),
195
+ areaChart: makeChartWidget('area'),
196
+ };
197
+ /** Merges consumer adapters over the defaults (consumer keys win on collision). */
198
+ export function mergeRegistries(...registries) {
199
+ return Object.assign({}, DEFAULT_REGISTRY, ...registries);
200
+ }
201
+ const unknownStyle = {
202
+ display: 'inline-flex',
203
+ alignItems: 'center',
204
+ gap: '0.375rem',
205
+ padding: '0.25rem 0.5rem',
206
+ borderRadius: '0.375rem',
207
+ borderWidth: '1px',
208
+ borderStyle: 'dashed',
209
+ borderColor: 'var(--colors-border-critical, currentColor)',
210
+ color: 'var(--colors-text-critical, currentColor)',
211
+ fontFamily: 'var(--fonts-mono, monospace)',
212
+ fontSize: '0.75rem',
213
+ };
214
+ /**
215
+ * Inline marker rendered in place of a widget whose ref or component key does
216
+ * not resolve — one bad manifest entry never blanks the whole dashboard.
217
+ */
218
+ export function UnknownWidget({ label }) {
219
+ return (_jsxs("span", { style: unknownStyle, role: "status", children: ["unknown widget: ", label] }));
220
+ }