@seed-ship/mcp-ui-solid 6.8.2 → 6.9.0
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/CHANGELOG.md +34 -0
- package/dist/components/ChartJSRenderer.cjs +27 -13
- package/dist/components/ChartJSRenderer.cjs.map +1 -1
- package/dist/components/ChartJSRenderer.d.ts.map +1 -1
- package/dist/components/ChartJSRenderer.js +28 -14
- package/dist/components/ChartJSRenderer.js.map +1 -1
- package/dist/components/DegradedFallback.cjs +73 -0
- package/dist/components/DegradedFallback.cjs.map +1 -0
- package/dist/components/DegradedFallback.d.ts +37 -0
- package/dist/components/DegradedFallback.d.ts.map +1 -0
- package/dist/components/DegradedFallback.js +73 -0
- package/dist/components/DegradedFallback.js.map +1 -0
- package/dist/components/GraphRenderer.cjs +30 -15
- package/dist/components/GraphRenderer.cjs.map +1 -1
- package/dist/components/GraphRenderer.d.ts.map +1 -1
- package/dist/components/GraphRenderer.js +31 -16
- package/dist/components/GraphRenderer.js.map +1 -1
- package/dist/components/MapRenderer.cjs +128 -107
- package/dist/components/MapRenderer.cjs.map +1 -1
- package/dist/components/MapRenderer.d.ts.map +1 -1
- package/dist/components/MapRenderer.js +129 -108
- package/dist/components/MapRenderer.js.map +1 -1
- package/dist/index.cjs +4 -4
- package/dist/index.js +1 -1
- package/dist/utils/degraded-projections.cjs +87 -0
- package/dist/utils/degraded-projections.cjs.map +1 -0
- package/dist/utils/degraded-projections.d.ts +64 -0
- package/dist/utils/degraded-projections.d.ts.map +1 -0
- package/dist/utils/degraded-projections.js +87 -0
- package/dist/utils/degraded-projections.js.map +1 -0
- package/package.json +1 -1
- package/src/components/ChartJSRenderer.tsx +94 -85
- package/src/components/DegradedFallback.test.tsx +61 -0
- package/src/components/DegradedFallback.tsx +93 -0
- package/src/components/GraphRenderer.tsx +26 -4
- package/src/components/MapRenderer.tsx +446 -392
- package/src/utils/degraded-projections.test.ts +113 -0
- package/src/utils/degraded-projections.ts +149 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the degraded-fallback projections (P2.5).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
graphToDegradedTable,
|
|
8
|
+
mapToDegradedTable,
|
|
9
|
+
chartToDegradedTable,
|
|
10
|
+
} from './degraded-projections';
|
|
11
|
+
|
|
12
|
+
describe('graphToDegradedTable', () => {
|
|
13
|
+
it('projects edges to a Source/Target/Label table when edges exist', () => {
|
|
14
|
+
const t = graphToDegradedTable({
|
|
15
|
+
nodes: [{ id: 'a' }, { id: 'b' }],
|
|
16
|
+
edges: [{ source: 'a', target: 'b', label: 'rel', weight: 3 }],
|
|
17
|
+
});
|
|
18
|
+
expect(t.columns).toEqual(['Source', 'Target', 'Label']);
|
|
19
|
+
expect(t.rows).toEqual([['a', 'b', '3 · rel']]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('falls back to a node list when there are no edges', () => {
|
|
23
|
+
const t = graphToDegradedTable({ nodes: [{ id: 'a', label: 'Alpha' }, { id: 'b' }] });
|
|
24
|
+
expect(t.columns).toEqual(['Node', 'Label']);
|
|
25
|
+
expect(t.rows).toEqual([
|
|
26
|
+
['a', 'Alpha'],
|
|
27
|
+
['b', 'b'],
|
|
28
|
+
]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('handles empty graph without throwing', () => {
|
|
32
|
+
const t = graphToDegradedTable({});
|
|
33
|
+
expect(t.rows).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('mapToDegradedTable', () => {
|
|
38
|
+
it('projects markers (tuple and object positions)', () => {
|
|
39
|
+
const t = mapToDegradedTable({
|
|
40
|
+
markers: [
|
|
41
|
+
{ position: [48.85, 2.35], tooltip: 'Paris' },
|
|
42
|
+
{ position: { lat: 45.76, lng: 4.84 }, popup: 'Lyon' },
|
|
43
|
+
],
|
|
44
|
+
});
|
|
45
|
+
expect(t.columns).toEqual(['Type', 'Lat', 'Lng', 'Info']);
|
|
46
|
+
expect(t.rows[0]).toEqual(['marker', '48.85', '2.35', 'Paris']);
|
|
47
|
+
expect(t.rows[1]).toEqual(['marker', '45.76', '4.84', 'Lyon']);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('projects GeoJSON features with a representative coord + props summary', () => {
|
|
51
|
+
const t = mapToDegradedTable({
|
|
52
|
+
geojson: {
|
|
53
|
+
type: 'FeatureCollection',
|
|
54
|
+
features: [
|
|
55
|
+
{
|
|
56
|
+
type: 'Feature',
|
|
57
|
+
geometry: {
|
|
58
|
+
type: 'Polygon',
|
|
59
|
+
coordinates: [
|
|
60
|
+
[
|
|
61
|
+
[2.3, 48.8],
|
|
62
|
+
[2.4, 48.8],
|
|
63
|
+
[2.4, 48.9],
|
|
64
|
+
[2.3, 48.8],
|
|
65
|
+
],
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
properties: { name: 'Zone A', value: 42 },
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
expect(t.rows[0][0]).toBe('Polygon');
|
|
74
|
+
expect(t.rows[0][1]).toBe('48.8'); // lat
|
|
75
|
+
expect(t.rows[0][2]).toBe('2.3'); // lng
|
|
76
|
+
expect(t.rows[0][3]).toContain('name=Zone A');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('handles a map with neither markers nor geojson', () => {
|
|
80
|
+
expect(mapToDegradedTable({}).rows).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('chartToDegradedTable', () => {
|
|
85
|
+
it('projects labels × datasets into a series table', () => {
|
|
86
|
+
const t = chartToDegradedTable({
|
|
87
|
+
data: {
|
|
88
|
+
labels: ['Q1', 'Q2'],
|
|
89
|
+
datasets: [
|
|
90
|
+
{ label: 'Revenue', data: [100, 150] },
|
|
91
|
+
{ label: 'Cost', data: [40, 60] },
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
expect(t.columns).toEqual(['', 'Revenue', 'Cost']);
|
|
96
|
+
expect(t.rows).toEqual([
|
|
97
|
+
['Q1', '100', '40'],
|
|
98
|
+
['Q2', '150', '60'],
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('names unlabeled datasets and stringifies point data', () => {
|
|
103
|
+
const t = chartToDegradedTable({
|
|
104
|
+
data: { datasets: [{ data: [{ x: 1, y: 2 }] }] },
|
|
105
|
+
});
|
|
106
|
+
expect(t.columns).toEqual(['', 'Series 1']);
|
|
107
|
+
expect(t.rows[0][1]).toBe('{"x":1,"y":2}');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('handles a chart with no data without throwing', () => {
|
|
111
|
+
expect(chartToDegradedTable({}).rows).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Degraded-fallback projections (audit 2026-05-30, P2.5).
|
|
3
|
+
*
|
|
4
|
+
* Pure functions that turn a heavy renderer's params into a flat
|
|
5
|
+
* `{ columns, rows }` table for `<DegradedFallback>` — the middle rung of
|
|
6
|
+
* the fallback ladder shown when the native render (G6 / Leaflet / Chart.js)
|
|
7
|
+
* is unavailable or throws. No peer deps, no side effects, fully testable.
|
|
8
|
+
*
|
|
9
|
+
* These are best-effort views: they surface the underlying data so the user
|
|
10
|
+
* isn't left with a blank space, not faithful reproductions of the chart/
|
|
11
|
+
* map/graph.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface DegradedTable {
|
|
15
|
+
columns: string[];
|
|
16
|
+
rows: Array<Array<string | number>>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MAX_PROJECTED_ROWS = 200;
|
|
20
|
+
|
|
21
|
+
/** Compact a value to a single table cell string. */
|
|
22
|
+
function cell(value: unknown): string {
|
|
23
|
+
if (value == null) return '';
|
|
24
|
+
if (typeof value === 'object') {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.stringify(value);
|
|
27
|
+
} catch {
|
|
28
|
+
return String(value);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return String(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── Graph ───────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Graph → edge table when edges exist (Source / Target / Label), else a
|
|
38
|
+
* node list (Node / Label). The graph renderer can therefore degrade to a
|
|
39
|
+
* readable relationship listing instead of a blank canvas.
|
|
40
|
+
*/
|
|
41
|
+
export function graphToDegradedTable(params: {
|
|
42
|
+
nodes?: Array<{ id: string; label?: string }>;
|
|
43
|
+
edges?: Array<{ source: string; target: string; label?: string; weight?: number }>;
|
|
44
|
+
}): DegradedTable {
|
|
45
|
+
const edges = params.edges ?? [];
|
|
46
|
+
if (edges.length > 0) {
|
|
47
|
+
return {
|
|
48
|
+
columns: ['Source', 'Target', 'Label'],
|
|
49
|
+
rows: edges.slice(0, MAX_PROJECTED_ROWS).map((e) => {
|
|
50
|
+
const label = [e.weight != null ? String(e.weight) : '', e.label ?? '']
|
|
51
|
+
.filter(Boolean)
|
|
52
|
+
.join(' · ');
|
|
53
|
+
return [cell(e.source), cell(e.target), label];
|
|
54
|
+
}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const nodes = params.nodes ?? [];
|
|
58
|
+
return {
|
|
59
|
+
columns: ['Node', 'Label'],
|
|
60
|
+
rows: nodes.slice(0, MAX_PROJECTED_ROWS).map((n) => [cell(n.id), cell(n.label ?? n.id)]),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Map ───────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
interface GeoJSONLikeFeature {
|
|
67
|
+
geometry?: { type?: string; coordinates?: unknown } | null;
|
|
68
|
+
properties?: Record<string, unknown> | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Pull a representative `[lng, lat]` pair out of a geometry's coordinates. */
|
|
72
|
+
function firstLngLat(coords: unknown): [number, number] | null {
|
|
73
|
+
let c: unknown = coords;
|
|
74
|
+
// Descend nested arrays until we reach a [number, number, ...] position.
|
|
75
|
+
while (Array.isArray(c) && Array.isArray(c[0])) c = c[0];
|
|
76
|
+
if (Array.isArray(c) && typeof c[0] === 'number' && typeof c[1] === 'number') {
|
|
77
|
+
return [c[0], c[1]];
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Map → a coordinate table. Markers become Lat/Lng/Label rows; GeoJSON
|
|
84
|
+
* features become Type / Lat / Lng (+ a compact properties summary). So a
|
|
85
|
+
* map that can't paint still lists where its points are.
|
|
86
|
+
*/
|
|
87
|
+
export function mapToDegradedTable(params: {
|
|
88
|
+
markers?: Array<{
|
|
89
|
+
position: [number, number] | { lat: number; lng: number };
|
|
90
|
+
tooltip?: string;
|
|
91
|
+
popup?: string;
|
|
92
|
+
}>;
|
|
93
|
+
geojson?: unknown;
|
|
94
|
+
}): DegradedTable {
|
|
95
|
+
const rows: Array<Array<string | number>> = [];
|
|
96
|
+
|
|
97
|
+
for (const m of params.markers ?? []) {
|
|
98
|
+
const lat = Array.isArray(m.position) ? m.position[0] : m.position?.lat;
|
|
99
|
+
const lng = Array.isArray(m.position) ? m.position[1] : m.position?.lng;
|
|
100
|
+
rows.push(['marker', cell(lat), cell(lng), cell(m.tooltip ?? m.popup ?? '')]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const fc = params.geojson as { features?: GeoJSONLikeFeature[] } | undefined;
|
|
104
|
+
const features = Array.isArray(fc?.features) ? fc!.features : [];
|
|
105
|
+
for (const f of features) {
|
|
106
|
+
const ll = firstLngLat(f.geometry?.coordinates);
|
|
107
|
+
const props = f.properties ?? {};
|
|
108
|
+
const propSummary = Object.keys(props)
|
|
109
|
+
.slice(0, 3)
|
|
110
|
+
.map((k) => `${k}=${cell(props[k])}`)
|
|
111
|
+
.join(', ');
|
|
112
|
+
rows.push([
|
|
113
|
+
cell(f.geometry?.type ?? 'feature'),
|
|
114
|
+
ll ? cell(ll[1]) : '',
|
|
115
|
+
ll ? cell(ll[0]) : '',
|
|
116
|
+
propSummary,
|
|
117
|
+
]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
columns: ['Type', 'Lat', 'Lng', 'Info'],
|
|
122
|
+
rows: rows.slice(0, MAX_PROJECTED_ROWS),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── Chart ───────────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Chart → a series table: one row per label, one column per dataset. So a
|
|
130
|
+
* chart that can't draw still shows its numbers. Point/object data (scatter,
|
|
131
|
+
* bubble, time series) is stringified per cell.
|
|
132
|
+
*/
|
|
133
|
+
export function chartToDegradedTable(params: {
|
|
134
|
+
data?: {
|
|
135
|
+
labels?: Array<string | number>;
|
|
136
|
+
datasets?: Array<{ label?: string; data?: unknown[] }>;
|
|
137
|
+
};
|
|
138
|
+
}): DegradedTable {
|
|
139
|
+
const datasets = params.data?.datasets ?? [];
|
|
140
|
+
const labels = params.data?.labels ?? [];
|
|
141
|
+
const rowCount = Math.max(labels.length, ...datasets.map((d) => d.data?.length ?? 0), 0);
|
|
142
|
+
|
|
143
|
+
const columns = ['', ...datasets.map((d, i) => d.label ?? `Series ${i + 1}`)];
|
|
144
|
+
const rows: Array<Array<string | number>> = [];
|
|
145
|
+
for (let r = 0; r < Math.min(rowCount, MAX_PROJECTED_ROWS); r++) {
|
|
146
|
+
rows.push([cell(labels[r] ?? r + 1), ...datasets.map((d) => cell(d.data?.[r]))]);
|
|
147
|
+
}
|
|
148
|
+
return { columns, rows };
|
|
149
|
+
}
|