@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.
@@ -0,0 +1,38 @@
1
+ import { type JSX } from 'react';
2
+ import { type InteractionContextValue } from './interaction.js';
3
+ import { type ComponentRegistry } from './registry.js';
4
+ import type { DashboardSpec, DashboardDataSources, LayoutNode } from './schema.js';
5
+ type RenderContext = {
6
+ widgets: DashboardSpec['widgets'];
7
+ dataSources: DashboardDataSources;
8
+ registry: ComponentRegistry;
9
+ interaction: InteractionContextValue;
10
+ };
11
+ export declare function renderLayoutNode(node: LayoutNode, ctx: RenderContext, key: string): JSX.Element;
12
+ export type DashboardRendererProps = {
13
+ spec: DashboardSpec;
14
+ dataSources: DashboardDataSources;
15
+ /**
16
+ * The string -> component registry. Defaults to {@link DEFAULT_REGISTRY};
17
+ * pass a merged registry (see `mergeRegistries`) to add domain adapters.
18
+ */
19
+ registry?: ComponentRegistry;
20
+ /**
21
+ * A real interaction store (e.g. charting-backed via
22
+ * `useChartingInteraction`). Omit to use a self-contained local store, which
23
+ * every standalone manifest (a story, a preview) can rely on unchanged.
24
+ */
25
+ interaction?: InteractionContextValue;
26
+ /**
27
+ * Skip the zod gate. Only for a spec the caller authored and trusts (where
28
+ * validation is a dev-time assertion, not a trust boundary); leave it on for
29
+ * any spec an agent or a command bar produced.
30
+ */
31
+ skipValidation?: boolean;
32
+ };
33
+ /**
34
+ * Renders a `DashboardSpec`. Validates first (unless `skipValidation`), showing
35
+ * an annotated rejection for an invalid manifest rather than throwing.
36
+ */
37
+ export declare function DashboardRenderer({ spec, dataSources, registry, interaction, skipValidation, }: DashboardRendererProps): JSX.Element;
38
+ export {};
@@ -0,0 +1,140 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Panel, SplitLayout } from '@r0hitsharma/design-system';
3
+ import {} from 'react';
4
+ import { useLocalInteraction, } from './interaction.js';
5
+ import { DEFAULT_REGISTRY, UnknownWidget, } from './registry.js';
6
+ import { validateDashboardSpec } from './validate.js';
7
+ /**
8
+ * The recursive `DashboardSpec` -> JSX renderer. Walks `spec.layout`
9
+ * (positions/nesting only — see schema.ts for why layout stays separate from
10
+ * widget config and data binding), resolving each `widget` leaf through
11
+ * `spec.widgets` and then through the string -> component `registry`. An
12
+ * unresolved widget ref or component key renders an inline `UnknownWidget`
13
+ * marker instead of throwing, so one bad manifest entry doesn't blank the
14
+ * whole dashboard.
15
+ *
16
+ * A `split` node with `resizable: true` becomes a design-system `SplitLayout`
17
+ * (an Ark `Splitter`), one drag-resizable panel per child; otherwise it's a
18
+ * plain flex box that wraps at narrow widths. Nesting works either way — each
19
+ * panel's content is just another recursive render.
20
+ */
21
+ /** Default height for a resizable split that declares none (a `column` split must; validation enforces it). */
22
+ const DEFAULT_RESIZABLE_HEIGHT = '420px';
23
+ const rootStyle = {
24
+ width: '100%',
25
+ minWidth: 0,
26
+ display: 'flex',
27
+ flexDirection: 'column',
28
+ gap: '1rem',
29
+ };
30
+ const titleStyle = {
31
+ fontSize: '1.125rem',
32
+ fontWeight: 600,
33
+ color: 'var(--colors-text-default, currentColor)',
34
+ margin: 0,
35
+ };
36
+ const invalidStyle = {
37
+ display: 'flex',
38
+ flexDirection: 'column',
39
+ gap: '0.5rem',
40
+ padding: '1rem',
41
+ borderRadius: '0.5rem',
42
+ borderWidth: '1px',
43
+ borderStyle: 'solid',
44
+ borderColor: 'var(--colors-border-critical, currentColor)',
45
+ background: 'var(--colors-bg-critical, transparent)',
46
+ color: 'var(--colors-text-critical, currentColor)',
47
+ fontSize: '0.875rem',
48
+ };
49
+ const issueListStyle = {
50
+ display: 'flex',
51
+ flexDirection: 'column',
52
+ gap: '0.25rem',
53
+ fontFamily: 'var(--fonts-mono, monospace)',
54
+ fontSize: '0.75rem',
55
+ listStyle: 'none',
56
+ padding: 0,
57
+ margin: 0,
58
+ };
59
+ /**
60
+ * What an invalid manifest renders instead of a dashboard: an annotated list,
61
+ * not a thrown error — the whole point of the gate is that an agent-submitted
62
+ * patch produces a readable rejection a human (or the agent) can act on.
63
+ */
64
+ function InvalidSpec({ issues }) {
65
+ return (_jsxs("div", { style: invalidStyle, role: "alert", children: [_jsxs("strong", { children: ["This dashboard manifest is invalid (", issues.length, " issue", issues.length === 1 ? '' : 's', ") and was not rendered."] }), _jsx("ul", { style: issueListStyle, children: issues.map((issue) => (_jsxs("li", { children: [_jsx("code", { children: issue.path }), " \u2014 ", issue.message] }, `${issue.path}:${issue.message}`))) })] }));
66
+ }
67
+ function renderWidget(ref, ctx) {
68
+ const widget = ctx.widgets[ref];
69
+ if (!widget) {
70
+ return (_jsx(UnknownWidget, { label: `layout references unknown widget "${ref}"` }));
71
+ }
72
+ const Adapter = ctx.registry[widget.component];
73
+ if (!Adapter) {
74
+ return _jsx(UnknownWidget, { label: widget.component });
75
+ }
76
+ const data = widget.dataBinding
77
+ ? (ctx.dataSources[widget.dataBinding.source] ?? [])
78
+ : [];
79
+ return (_jsx(Panel, { title: widget.title, style: { width: '100%', minWidth: 0 }, "data-widget-id": widget.id, "data-widget-component": widget.component, children: _jsx(Adapter, { widget: widget, data: data, interaction: ctx.interaction }) }));
80
+ }
81
+ function splitContainerStyle(direction) {
82
+ return {
83
+ display: 'flex',
84
+ flexDirection: direction,
85
+ flexWrap: direction === 'row' ? 'wrap' : 'nowrap',
86
+ gap: '1rem',
87
+ width: '100%',
88
+ minWidth: 0,
89
+ };
90
+ }
91
+ function ResizableSplit({ node, ctx, nodeKey, }) {
92
+ const panels = node.children.map((child, index) => ({
93
+ id: `${nodeKey}.${index}`,
94
+ size: child.size ?? 1,
95
+ content: renderLayoutNode(child, ctx, `${nodeKey}.${index}`),
96
+ }));
97
+ const height = node.height ?? DEFAULT_RESIZABLE_HEIGHT;
98
+ return (_jsx("div", { style: { width: '100%', minWidth: 0, height }, children: _jsx(SplitLayout, { orientation: node.direction === 'row' ? 'horizontal' : 'vertical', panels: panels }) }));
99
+ }
100
+ export function renderLayoutNode(node, ctx, key) {
101
+ if (node.type === 'widget') {
102
+ return (_jsx("div", { style: {
103
+ display: 'flex',
104
+ flexDirection: 'column',
105
+ minWidth: '280px',
106
+ flexGrow: node.size ?? 1,
107
+ flexBasis: 0,
108
+ }, children: renderWidget(node.ref, ctx) }, key));
109
+ }
110
+ const inner = node.resizable ? (_jsx(ResizableSplit, { node: node, ctx: ctx, nodeKey: key })) : (_jsx("div", { style: splitContainerStyle(node.direction), children: node.children.map((child, index) => renderLayoutNode(child, ctx, `${key}.${index}`)) }));
111
+ return (_jsx("div", { style: {
112
+ display: 'flex',
113
+ flexDirection: 'column',
114
+ minWidth: 0,
115
+ flexGrow: node.size ?? 1,
116
+ flexBasis: 0,
117
+ }, children: inner }, key));
118
+ }
119
+ /**
120
+ * Renders a `DashboardSpec`. Validates first (unless `skipValidation`), showing
121
+ * an annotated rejection for an invalid manifest rather than throwing.
122
+ */
123
+ export function DashboardRenderer({ spec, dataSources, registry = DEFAULT_REGISTRY, interaction, skipValidation = false, }) {
124
+ const localInteraction = useLocalInteraction();
125
+ const resolvedInteraction = interaction ?? localInteraction;
126
+ if (!skipValidation) {
127
+ const result = validateDashboardSpec(spec, {
128
+ knownComponents: Object.keys(registry),
129
+ });
130
+ if (!result.ok)
131
+ return _jsx(InvalidSpec, { issues: result.issues });
132
+ }
133
+ const ctx = {
134
+ widgets: spec.widgets,
135
+ dataSources,
136
+ registry,
137
+ interaction: resolvedInteraction,
138
+ };
139
+ return (_jsxs("div", { style: rootStyle, children: [spec.title ? _jsx("h2", { style: titleStyle, children: spec.title }) : null, renderLayoutNode(spec.layout, ctx, 'root')] }));
140
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * `@r0hitsharma/dashboard-kit` — the generic, manifest-driven dashboard
3
+ * engine.
4
+ *
5
+ * Three concerns stay SEPARABLE rather than conflated the way Grafana (flat,
6
+ * absolute `gridPos` + string-interpolated query blobs) or Superset (three
7
+ * disconnected config blobs) do:
8
+ *
9
+ * 1. `layout` — a recursive composition tree (Vega-Lite `hconcat`/
10
+ * `vconcat` style), independent of what each leaf renders.
11
+ * 2. `widgets` — a flat, id-keyed registry of widget nodes (`{component,
12
+ * props, dataBinding}`), each resolved through a string -> component
13
+ * registry key (see `registry.tsx`).
14
+ * 3. `dataBinding` — a typed `{source, fields}` pointer at a data source,
15
+ * uniform across whatever transport a consuming app resolves `source`
16
+ * ids against (live tail, replay cursor, a REST poll, ...).
17
+ *
18
+ * The one addition none of Grafana/Superset/Perspective/Vega-Lite model as
19
+ * part of the *widget* node itself is `interaction.reads`/`writes`: the
20
+ * shared-selection keys a widget consumes or produces. Declaring it here
21
+ * makes cross-filtering a declared, auditable property of the manifest
22
+ * instead of implicit wiring buried in component code — see `interaction.ts`
23
+ * for how those keys reach a real interaction store.
24
+ */
25
+ export type LayoutDirection = 'row' | 'column';
26
+ /** A recursive split node — maps onto the design-system's `SplitLayout` orientation. */
27
+ export type SplitLayoutNode = {
28
+ type: 'split';
29
+ direction: LayoutDirection;
30
+ /** Relative share of the parent split's main axis; children need not sum to 1. */
31
+ size?: number;
32
+ /**
33
+ * Render this split as a DRAG-RESIZABLE panel group (design-system's
34
+ * `SplitLayout`, itself an Ark `Splitter`) instead of a plain flex box,
35
+ * with one panel per child and a resize handle between each pair. N-way by
36
+ * construction: three children give two handles.
37
+ *
38
+ * Opt-in rather than the default, because a `SplitLayout` needs hard panel
39
+ * sizes and therefore cannot flex-wrap: the non-resizable path keeps the
40
+ * responsive wrapping some dashboards rely on at narrow widths. A
41
+ * `direction: 'column'` split additionally needs {@link height}, since a
42
+ * vertical splitter has no content-driven size to divide.
43
+ */
44
+ resizable?: boolean;
45
+ /** Required for a resizable `column` split: the CSS height to divide. */
46
+ height?: string;
47
+ children: LayoutNode[];
48
+ };
49
+ /** A leaf that resolves to one entry in the flat `widgets` registry. */
50
+ export type WidgetLayoutNode = {
51
+ type: 'widget';
52
+ /** Key into `DashboardSpec['widgets']`. */
53
+ ref: string;
54
+ /** Relative share of the parent split's main axis. */
55
+ size?: number;
56
+ };
57
+ export type LayoutNode = SplitLayoutNode | WidgetLayoutNode;
58
+ /**
59
+ * What data source feeds a widget, and how its fields map onto the shape the
60
+ * component needs — a typed channel-to-field mapping (Vega-Lite style)
61
+ * rather than an opaque query string. `source` ids are resolved through
62
+ * whatever {@link DashboardDataSources} table the host app supplies;
63
+ * `dashboard-kit` itself is agnostic to what backs a source (a mock array, a
64
+ * live tail, a replay cursor).
65
+ */
66
+ export type DataBinding = {
67
+ /** Data-source id, e.g. `"stream:metrics.throughput"`. */
68
+ source: string;
69
+ /** Field name mapping: which field of a source record feeds which slot. */
70
+ fields: Record<string, string>;
71
+ };
72
+ /**
73
+ * Shared-selection keys a widget consumes (`reads`) or produces (`writes`).
74
+ * Symmetric by design — any widget may read or write any key, no
75
+ * master/detail hardcoding.
76
+ */
77
+ export type WidgetInteraction = {
78
+ reads?: string[];
79
+ writes?: string[];
80
+ /**
81
+ * Per-field AGENT EXPOSURE: which of this widget's interaction keys an
82
+ * agent (a drive tool, a command bar) may write. **Default-deny**: a key a
83
+ * human can write through the UI is NOT agent-writable unless it is listed
84
+ * here.
85
+ *
86
+ * This is the manifest's half of the contract; a host app's agent-facing
87
+ * tools consult {@link collectAgentWritableKeys} and refuse anything
88
+ * outside it, so the agent surface is a declared, auditable property of
89
+ * the dashboard rather than "whatever setters happen to be in scope".
90
+ */
91
+ agentWritable?: string[];
92
+ };
93
+ export type ThresholdSeverity = 'success' | 'warning' | 'critical';
94
+ export type ThresholdRule = {
95
+ op: 'gte' | 'lte' | 'gt' | 'lt' | 'eq';
96
+ value: number;
97
+ severity: ThresholdSeverity;
98
+ };
99
+ /** One declarative column for a table-rendering widget. */
100
+ export type WidgetTableColumn = {
101
+ accessorKey: string;
102
+ header: string;
103
+ /** How the registry's table adapter should render the cell. */
104
+ render?: 'text' | 'number' | 'currency' | 'percent' | 'badge' | 'sparkline';
105
+ };
106
+ export type WidgetNode = {
107
+ id: string;
108
+ /** Component-registry key, e.g. `"panel.stat"` or `"chart.line"`. */
109
+ component: string;
110
+ title?: string;
111
+ /**
112
+ * Component-specific configuration, passed through to the resolved
113
+ * component-registry entry. Kept to JSON-serializable shapes (no
114
+ * functions) so a manifest stays a genuine, inspectable data value —
115
+ * `columns` for table widgets is the one structured exception.
116
+ */
117
+ props?: Record<string, unknown> & {
118
+ columns?: WidgetTableColumn[];
119
+ };
120
+ dataBinding?: DataBinding;
121
+ interaction?: WidgetInteraction;
122
+ thresholds?: ThresholdRule[];
123
+ };
124
+ export type DashboardSpec = {
125
+ version: 1;
126
+ title?: string;
127
+ layout: LayoutNode;
128
+ /** Flat registry of widget nodes, referenced by id from `layout`. */
129
+ widgets: Record<string, WidgetNode>;
130
+ };
131
+ /**
132
+ * Data-source table keyed by `DataBinding['source']` — every source resolves
133
+ * to an array of plain records; scalar widgets (e.g. a stat tile reading a
134
+ * gauge) read the last record. A host app owns populating this (from a mock
135
+ * fixture, a live subscription snapshot, a query result, ...); the engine
136
+ * only ever reads it.
137
+ */
138
+ export type DashboardDataSources = Record<string, Record<string, unknown>[]>;
package/dist/schema.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * `@r0hitsharma/dashboard-kit` — the generic, manifest-driven dashboard
3
+ * engine.
4
+ *
5
+ * Three concerns stay SEPARABLE rather than conflated the way Grafana (flat,
6
+ * absolute `gridPos` + string-interpolated query blobs) or Superset (three
7
+ * disconnected config blobs) do:
8
+ *
9
+ * 1. `layout` — a recursive composition tree (Vega-Lite `hconcat`/
10
+ * `vconcat` style), independent of what each leaf renders.
11
+ * 2. `widgets` — a flat, id-keyed registry of widget nodes (`{component,
12
+ * props, dataBinding}`), each resolved through a string -> component
13
+ * registry key (see `registry.tsx`).
14
+ * 3. `dataBinding` — a typed `{source, fields}` pointer at a data source,
15
+ * uniform across whatever transport a consuming app resolves `source`
16
+ * ids against (live tail, replay cursor, a REST poll, ...).
17
+ *
18
+ * The one addition none of Grafana/Superset/Perspective/Vega-Lite model as
19
+ * part of the *widget* node itself is `interaction.reads`/`writes`: the
20
+ * shared-selection keys a widget consumes or produces. Declaring it here
21
+ * makes cross-filtering a declared, auditable property of the manifest
22
+ * instead of implicit wiring buried in component code — see `interaction.ts`
23
+ * for how those keys reach a real interaction store.
24
+ */
25
+ export {};
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Manifest validation — the gate that must land before any "an agent or
3
+ * command bar submits a manifest patch" feature can trust a `DashboardSpec`.
4
+ *
5
+ * Two layers, because zod alone can't catch the interesting failures:
6
+ *
7
+ * 1. STRUCTURAL — `dashboardSpecSchema` below. Shape, enums, required
8
+ * fields, no malformed widget nodes.
9
+ * 2. REFERENTIAL — {@link validateDashboardSpec}'s extra passes. Every
10
+ * `layout` leaf must name a widget that exists; every widget must be
11
+ * reachable from the layout; a resizable `column` split must carry a
12
+ * `height`; an `agentWritable` key must actually be writable. These are
13
+ * the mistakes an agent-authored patch actually makes, and none of them
14
+ * are expressible as a type.
15
+ *
16
+ * `zod` deliberately does NOT validate `component` against a registry here:
17
+ * `schema.ts` is the data contract and a registry is one possible resolution
18
+ * of it. The renderer degrades gracefully on an unknown component key (it
19
+ * renders an `UnknownWidget` marker), and coupling the two would make the
20
+ * schema un-shareable. {@link validateDashboardSpec} takes an optional set of
21
+ * known component keys for callers who DO want that checked.
22
+ */
23
+ import { z } from 'zod';
24
+ import type { DashboardSpec } from './schema.js';
25
+ export declare const dashboardSpecSchema: z.ZodObject<{
26
+ version: z.ZodLiteral<1>;
27
+ title: z.ZodOptional<z.ZodString>;
28
+ layout: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
29
+ widgets: z.ZodRecord<z.ZodString, z.ZodObject<{
30
+ id: z.ZodString;
31
+ component: z.ZodString;
32
+ title: z.ZodOptional<z.ZodString>;
33
+ props: z.ZodOptional<z.ZodObject<{
34
+ columns: z.ZodOptional<z.ZodArray<z.ZodObject<{
35
+ accessorKey: z.ZodString;
36
+ header: z.ZodString;
37
+ render: z.ZodOptional<z.ZodEnum<{
38
+ badge: "badge";
39
+ currency: "currency";
40
+ number: "number";
41
+ percent: "percent";
42
+ sparkline: "sparkline";
43
+ text: "text";
44
+ }>>;
45
+ }, z.core.$strip>>>;
46
+ }, z.core.$loose>>;
47
+ dataBinding: z.ZodOptional<z.ZodObject<{
48
+ source: z.ZodString;
49
+ fields: z.ZodRecord<z.ZodString, z.ZodString>;
50
+ }, z.core.$strip>>;
51
+ interaction: z.ZodOptional<z.ZodObject<{
52
+ reads: z.ZodOptional<z.ZodArray<z.ZodString>>;
53
+ writes: z.ZodOptional<z.ZodArray<z.ZodString>>;
54
+ agentWritable: z.ZodOptional<z.ZodArray<z.ZodString>>;
55
+ }, z.core.$strip>>;
56
+ thresholds: z.ZodOptional<z.ZodArray<z.ZodObject<{
57
+ op: z.ZodEnum<{
58
+ eq: "eq";
59
+ gt: "gt";
60
+ gte: "gte";
61
+ lt: "lt";
62
+ lte: "lte";
63
+ }>;
64
+ value: z.ZodNumber;
65
+ severity: z.ZodEnum<{
66
+ critical: "critical";
67
+ success: "success";
68
+ warning: "warning";
69
+ }>;
70
+ }, z.core.$strip>>>;
71
+ }, z.core.$strip>>;
72
+ }, z.core.$strip>;
73
+ export type ManifestIssue = {
74
+ /** Dotted path into the spec, e.g. `widgets.exposure.component`. */
75
+ path: string;
76
+ message: string;
77
+ };
78
+ export type ManifestValidation = {
79
+ ok: true;
80
+ spec: DashboardSpec;
81
+ issues: [];
82
+ } | {
83
+ ok: false;
84
+ issues: ManifestIssue[];
85
+ };
86
+ export type ValidateOptions = {
87
+ /**
88
+ * When supplied, every widget's `component` must be one of these keys.
89
+ * Pass `Object.keys(registry)` to catch a typo'd or hallucinated component
90
+ * name before it renders as an `UnknownWidget` placeholder.
91
+ */
92
+ knownComponents?: Iterable<string>;
93
+ };
94
+ /**
95
+ * Validates a candidate manifest. Never throws — an agent-submitted patch is
96
+ * untrusted input, and the caller needs the reasons, not a stack trace.
97
+ */
98
+ export declare function validateDashboardSpec(candidate: unknown, { knownComponents }?: ValidateOptions): ManifestValidation;
99
+ /**
100
+ * The set of interaction keys an agent is allowed to write, collected across
101
+ * every widget in the manifest. Default-deny: an empty set means an agent may
102
+ * read the dashboard but drive nothing.
103
+ */
104
+ export declare function collectAgentWritableKeys(spec: DashboardSpec): Set<string>;
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Manifest validation — the gate that must land before any "an agent or
3
+ * command bar submits a manifest patch" feature can trust a `DashboardSpec`.
4
+ *
5
+ * Two layers, because zod alone can't catch the interesting failures:
6
+ *
7
+ * 1. STRUCTURAL — `dashboardSpecSchema` below. Shape, enums, required
8
+ * fields, no malformed widget nodes.
9
+ * 2. REFERENTIAL — {@link validateDashboardSpec}'s extra passes. Every
10
+ * `layout` leaf must name a widget that exists; every widget must be
11
+ * reachable from the layout; a resizable `column` split must carry a
12
+ * `height`; an `agentWritable` key must actually be writable. These are
13
+ * the mistakes an agent-authored patch actually makes, and none of them
14
+ * are expressible as a type.
15
+ *
16
+ * `zod` deliberately does NOT validate `component` against a registry here:
17
+ * `schema.ts` is the data contract and a registry is one possible resolution
18
+ * of it. The renderer degrades gracefully on an unknown component key (it
19
+ * renders an `UnknownWidget` marker), and coupling the two would make the
20
+ * schema un-shareable. {@link validateDashboardSpec} takes an optional set of
21
+ * known component keys for callers who DO want that checked.
22
+ */
23
+ import { z } from 'zod';
24
+ const layoutDirection = z.enum(['row', 'column']);
25
+ const widgetLayoutNode = z.object({
26
+ type: z.literal('widget'),
27
+ ref: z.string().min(1),
28
+ size: z.number().positive().optional(),
29
+ });
30
+ /**
31
+ * Recursive by `z.lazy` — a split's children are layout nodes, which may be
32
+ * splits.
33
+ */
34
+ const layoutNode = z.lazy(() => z.union([widgetLayoutNode, splitLayoutNode]));
35
+ const splitLayoutNode = z.object({
36
+ type: z.literal('split'),
37
+ direction: layoutDirection,
38
+ size: z.number().positive().optional(),
39
+ resizable: z.boolean().optional(),
40
+ height: z.string().min(1).optional(),
41
+ children: z.array(layoutNode).min(1),
42
+ });
43
+ const dataBinding = z.object({
44
+ source: z.string().min(1),
45
+ fields: z.record(z.string(), z.string()),
46
+ });
47
+ const widgetInteraction = z.object({
48
+ reads: z.array(z.string().min(1)).optional(),
49
+ writes: z.array(z.string().min(1)).optional(),
50
+ agentWritable: z.array(z.string().min(1)).optional(),
51
+ });
52
+ const thresholdRule = z.object({
53
+ op: z.enum(['gte', 'lte', 'gt', 'lt', 'eq']),
54
+ value: z.number(),
55
+ severity: z.enum(['success', 'warning', 'critical']),
56
+ });
57
+ const widgetTableColumn = z.object({
58
+ accessorKey: z.string().min(1),
59
+ header: z.string(),
60
+ render: z
61
+ .enum(['text', 'number', 'currency', 'percent', 'badge', 'sparkline'])
62
+ .optional(),
63
+ });
64
+ const widgetNode = z.object({
65
+ id: z.string().min(1),
66
+ component: z.string().min(1),
67
+ title: z.string().optional(),
68
+ // `props` stays open (it's per-component config) EXCEPT `columns`, which is
69
+ // structured enough — and mistyped often enough — to be worth checking.
70
+ props: z
71
+ .looseObject({ columns: z.array(widgetTableColumn).optional() })
72
+ .optional(),
73
+ dataBinding: dataBinding.optional(),
74
+ interaction: widgetInteraction.optional(),
75
+ thresholds: z.array(thresholdRule).optional(),
76
+ });
77
+ export const dashboardSpecSchema = z.object({
78
+ version: z.literal(1),
79
+ title: z.string().optional(),
80
+ layout: layoutNode,
81
+ widgets: z.record(z.string(), widgetNode),
82
+ });
83
+ function walkLayout(node, path, visit) {
84
+ visit(node, path);
85
+ node.children?.forEach((child, index) => walkLayout(child, `${path}.children[${index}]`, visit));
86
+ }
87
+ /**
88
+ * Validates a candidate manifest. Never throws — an agent-submitted patch is
89
+ * untrusted input, and the caller needs the reasons, not a stack trace.
90
+ */
91
+ export function validateDashboardSpec(candidate, { knownComponents } = {}) {
92
+ const parsed = dashboardSpecSchema.safeParse(candidate);
93
+ if (!parsed.success) {
94
+ return {
95
+ ok: false,
96
+ issues: parsed.error.issues.map((issue) => ({
97
+ path: issue.path.join('.') || '(root)',
98
+ message: issue.message,
99
+ })),
100
+ };
101
+ }
102
+ const spec = parsed.data;
103
+ const issues = [];
104
+ const known = knownComponents ? new Set(knownComponents) : null;
105
+ // ── Referential pass ────────────────────────────────────────────────────
106
+ const referenced = new Set();
107
+ walkLayout(spec.layout, 'layout', (node, path) => {
108
+ if (node.type === 'widget') {
109
+ const ref = node.ref;
110
+ referenced.add(ref);
111
+ if (!(ref in spec.widgets)) {
112
+ issues.push({
113
+ path: `${path}.ref`,
114
+ message: `layout references unknown widget "${ref}" — add it to \`widgets\` or fix the ref`,
115
+ });
116
+ }
117
+ return;
118
+ }
119
+ if (node.resizable && node.direction === 'column' && !node.height) {
120
+ issues.push({
121
+ path: `${path}.height`,
122
+ message: 'a resizable `column` split needs an explicit `height` — a vertical splitter has no content-driven size to divide',
123
+ });
124
+ }
125
+ });
126
+ for (const [key, widget] of Object.entries(spec.widgets)) {
127
+ if (widget.id !== key) {
128
+ issues.push({
129
+ path: `widgets.${key}.id`,
130
+ message: `widget id "${widget.id}" does not match its registry key "${key}"`,
131
+ });
132
+ }
133
+ if (!referenced.has(key)) {
134
+ issues.push({
135
+ path: `widgets.${key}`,
136
+ message: `widget "${key}" is never placed by \`layout\` — it will not render`,
137
+ });
138
+ }
139
+ if (known && !known.has(widget.component)) {
140
+ issues.push({
141
+ path: `widgets.${key}.component`,
142
+ message: `unknown component "${widget.component}"`,
143
+ });
144
+ }
145
+ // An agent-writable key that the widget cannot actually write is a policy
146
+ // that silently does nothing — worth catching at the gate. `writes` is the
147
+ // set of keys a widget produces; an `agentWritable` key must be one of them.
148
+ const writes = widget.interaction?.writes ?? [];
149
+ for (const exposed of widget.interaction?.agentWritable ?? []) {
150
+ if (!writes.includes(exposed)) {
151
+ issues.push({
152
+ path: `widgets.${key}.interaction.agentWritable`,
153
+ message: `"${exposed}" is marked agent-writable but is not in this widget's \`writes\``,
154
+ });
155
+ }
156
+ }
157
+ }
158
+ return issues.length > 0
159
+ ? { ok: false, issues }
160
+ : { ok: true, spec, issues: [] };
161
+ }
162
+ /**
163
+ * The set of interaction keys an agent is allowed to write, collected across
164
+ * every widget in the manifest. Default-deny: an empty set means an agent may
165
+ * read the dashboard but drive nothing.
166
+ */
167
+ export function collectAgentWritableKeys(spec) {
168
+ const keys = new Set();
169
+ for (const widget of Object.values(spec.widgets)) {
170
+ for (const key of widget.interaction?.agentWritable ?? [])
171
+ keys.add(key);
172
+ }
173
+ return keys;
174
+ }
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@r0hitsharma/dashboard-kit",
3
+ "publishConfig": {
4
+ "access": "public"
5
+ },
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "files": [
9
+ "dist",
10
+ "src"
11
+ ],
12
+ "main": "dist/index.js",
13
+ "types": "dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "default": "./dist/index.js"
18
+ }
19
+ },
20
+ "scripts": {
21
+ "build": "tsc -p tsconfig.build.json",
22
+ "clean": "rm -rf dist",
23
+ "prepare": "npm run build",
24
+ "type:check": "tsc -p tsconfig.json --noEmit",
25
+ "lint": "oxlint -c oxlint.config.ts --max-warnings=0 src",
26
+ "lint:fix": "oxlint -c oxlint.config.ts --fix src",
27
+ "format": "oxfmt -c oxfmt.config.ts --write src",
28
+ "format:check": "oxfmt -c oxfmt.config.ts --check src",
29
+ "test": "vitest run"
30
+ },
31
+ "peerDependencies": {
32
+ "@r0hitsharma/charting": "*",
33
+ "@r0hitsharma/design-system": "*",
34
+ "react": "^19.0.0",
35
+ "react-dom": "^19.0.0"
36
+ },
37
+ "dependencies": {
38
+ "@tanstack/react-table": "9.2.4",
39
+ "zod": "4.5.4"
40
+ },
41
+ "devDependencies": {
42
+ "@r0hitsharma/charting": "*",
43
+ "@r0hitsharma/design-system": "*",
44
+ "@r0hitsharma/oxfmt-config": "*",
45
+ "@r0hitsharma/oxlint-config": "*",
46
+ "@r0hitsharma/tsconfig": "*",
47
+ "@testing-library/react": "^16.3.2",
48
+ "@types/react": "19.2.18",
49
+ "@types/react-dom": "19.2.7",
50
+ "jsdom": "^30.0.0",
51
+ "oxfmt": "0.67.0",
52
+ "oxlint": "1.82.0",
53
+ "react": "^19.0.0",
54
+ "react-dom": "^19.0.0",
55
+ "vitest": "^4.1.9"
56
+ },
57
+ "version": "0.12.0-rohit-fork-ci.1",
58
+ "repository": {
59
+ "url": "https://github.com/r0hitsharma/uikit"
60
+ }
61
+ }