@seed-ship/mcp-ui-solid 6.8.1 → 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.
Files changed (46) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/components/ChartJSRenderer.cjs +27 -13
  3. package/dist/components/ChartJSRenderer.cjs.map +1 -1
  4. package/dist/components/ChartJSRenderer.d.ts.map +1 -1
  5. package/dist/components/ChartJSRenderer.js +28 -14
  6. package/dist/components/ChartJSRenderer.js.map +1 -1
  7. package/dist/components/DegradedFallback.cjs +73 -0
  8. package/dist/components/DegradedFallback.cjs.map +1 -0
  9. package/dist/components/DegradedFallback.d.ts +37 -0
  10. package/dist/components/DegradedFallback.d.ts.map +1 -0
  11. package/dist/components/DegradedFallback.js +73 -0
  12. package/dist/components/DegradedFallback.js.map +1 -0
  13. package/dist/components/GraphRenderer.cjs +30 -15
  14. package/dist/components/GraphRenderer.cjs.map +1 -1
  15. package/dist/components/GraphRenderer.d.ts.map +1 -1
  16. package/dist/components/GraphRenderer.js +31 -16
  17. package/dist/components/GraphRenderer.js.map +1 -1
  18. package/dist/components/MapRenderer.cjs +128 -107
  19. package/dist/components/MapRenderer.cjs.map +1 -1
  20. package/dist/components/MapRenderer.d.ts.map +1 -1
  21. package/dist/components/MapRenderer.js +129 -108
  22. package/dist/components/MapRenderer.js.map +1 -1
  23. package/dist/index.cjs +4 -4
  24. package/dist/index.js +1 -1
  25. package/dist/services/validation.cjs +43 -9
  26. package/dist/services/validation.cjs.map +1 -1
  27. package/dist/services/validation.d.ts.map +1 -1
  28. package/dist/services/validation.js +43 -9
  29. package/dist/services/validation.js.map +1 -1
  30. package/dist/utils/degraded-projections.cjs +87 -0
  31. package/dist/utils/degraded-projections.cjs.map +1 -0
  32. package/dist/utils/degraded-projections.d.ts +64 -0
  33. package/dist/utils/degraded-projections.d.ts.map +1 -0
  34. package/dist/utils/degraded-projections.js +87 -0
  35. package/dist/utils/degraded-projections.js.map +1 -0
  36. package/package.json +1 -1
  37. package/src/components/ChartJSRenderer.tsx +94 -85
  38. package/src/components/DegradedFallback.test.tsx +61 -0
  39. package/src/components/DegradedFallback.tsx +93 -0
  40. package/src/components/GraphRenderer.tsx +26 -4
  41. package/src/components/MapRenderer.tsx +446 -392
  42. package/src/services/validation.test.ts +298 -232
  43. package/src/services/validation.ts +210 -136
  44. package/src/utils/degraded-projections.test.ts +113 -0
  45. package/src/utils/degraded-projections.ts +149 -0
  46. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const MAX_PROJECTED_ROWS = 200;
4
+ function cell(value) {
5
+ if (value == null) return "";
6
+ if (typeof value === "object") {
7
+ try {
8
+ return JSON.stringify(value);
9
+ } catch {
10
+ return String(value);
11
+ }
12
+ }
13
+ return String(value);
14
+ }
15
+ function graphToDegradedTable(params) {
16
+ const edges = params.edges ?? [];
17
+ if (edges.length > 0) {
18
+ return {
19
+ columns: ["Source", "Target", "Label"],
20
+ rows: edges.slice(0, MAX_PROJECTED_ROWS).map((e) => {
21
+ const label = [e.weight != null ? String(e.weight) : "", e.label ?? ""].filter(Boolean).join(" · ");
22
+ return [cell(e.source), cell(e.target), label];
23
+ })
24
+ };
25
+ }
26
+ const nodes = params.nodes ?? [];
27
+ return {
28
+ columns: ["Node", "Label"],
29
+ rows: nodes.slice(0, MAX_PROJECTED_ROWS).map((n) => [cell(n.id), cell(n.label ?? n.id)])
30
+ };
31
+ }
32
+ function firstLngLat(coords) {
33
+ let c = coords;
34
+ while (Array.isArray(c) && Array.isArray(c[0])) c = c[0];
35
+ if (Array.isArray(c) && typeof c[0] === "number" && typeof c[1] === "number") {
36
+ return [c[0], c[1]];
37
+ }
38
+ return null;
39
+ }
40
+ function mapToDegradedTable(params) {
41
+ var _a, _b, _c, _d;
42
+ const rows = [];
43
+ for (const m of params.markers ?? []) {
44
+ const lat = Array.isArray(m.position) ? m.position[0] : (_a = m.position) == null ? void 0 : _a.lat;
45
+ const lng = Array.isArray(m.position) ? m.position[1] : (_b = m.position) == null ? void 0 : _b.lng;
46
+ rows.push(["marker", cell(lat), cell(lng), cell(m.tooltip ?? m.popup ?? "")]);
47
+ }
48
+ const fc = params.geojson;
49
+ const features = Array.isArray(fc == null ? void 0 : fc.features) ? fc.features : [];
50
+ for (const f of features) {
51
+ const ll = firstLngLat((_c = f.geometry) == null ? void 0 : _c.coordinates);
52
+ const props = f.properties ?? {};
53
+ const propSummary = Object.keys(props).slice(0, 3).map((k) => `${k}=${cell(props[k])}`).join(", ");
54
+ rows.push([
55
+ cell(((_d = f.geometry) == null ? void 0 : _d.type) ?? "feature"),
56
+ ll ? cell(ll[1]) : "",
57
+ ll ? cell(ll[0]) : "",
58
+ propSummary
59
+ ]);
60
+ }
61
+ return {
62
+ columns: ["Type", "Lat", "Lng", "Info"],
63
+ rows: rows.slice(0, MAX_PROJECTED_ROWS)
64
+ };
65
+ }
66
+ function chartToDegradedTable(params) {
67
+ var _a, _b;
68
+ const datasets = ((_a = params.data) == null ? void 0 : _a.datasets) ?? [];
69
+ const labels = ((_b = params.data) == null ? void 0 : _b.labels) ?? [];
70
+ const rowCount = Math.max(labels.length, ...datasets.map((d) => {
71
+ var _a2;
72
+ return ((_a2 = d.data) == null ? void 0 : _a2.length) ?? 0;
73
+ }), 0);
74
+ const columns = ["", ...datasets.map((d, i) => d.label ?? `Series ${i + 1}`)];
75
+ const rows = [];
76
+ for (let r = 0; r < Math.min(rowCount, MAX_PROJECTED_ROWS); r++) {
77
+ rows.push([cell(labels[r] ?? r + 1), ...datasets.map((d) => {
78
+ var _a2;
79
+ return cell((_a2 = d.data) == null ? void 0 : _a2[r]);
80
+ })]);
81
+ }
82
+ return { columns, rows };
83
+ }
84
+ exports.chartToDegradedTable = chartToDegradedTable;
85
+ exports.graphToDegradedTable = graphToDegradedTable;
86
+ exports.mapToDegradedTable = mapToDegradedTable;
87
+ //# sourceMappingURL=degraded-projections.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"degraded-projections.cjs","sources":["../../src/utils/degraded-projections.ts"],"sourcesContent":["/**\n * Degraded-fallback projections (audit 2026-05-30, P2.5).\n *\n * Pure functions that turn a heavy renderer's params into a flat\n * `{ columns, rows }` table for `<DegradedFallback>` — the middle rung of\n * the fallback ladder shown when the native render (G6 / Leaflet / Chart.js)\n * is unavailable or throws. No peer deps, no side effects, fully testable.\n *\n * These are best-effort views: they surface the underlying data so the user\n * isn't left with a blank space, not faithful reproductions of the chart/\n * map/graph.\n */\n\nexport interface DegradedTable {\n columns: string[];\n rows: Array<Array<string | number>>;\n}\n\nconst MAX_PROJECTED_ROWS = 200;\n\n/** Compact a value to a single table cell string. */\nfunction cell(value: unknown): string {\n if (value == null) return '';\n if (typeof value === 'object') {\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n }\n return String(value);\n}\n\n// ─── Graph ───────────────────────────────────────────────────────────────\n\n/**\n * Graph → edge table when edges exist (Source / Target / Label), else a\n * node list (Node / Label). The graph renderer can therefore degrade to a\n * readable relationship listing instead of a blank canvas.\n */\nexport function graphToDegradedTable(params: {\n nodes?: Array<{ id: string; label?: string }>;\n edges?: Array<{ source: string; target: string; label?: string; weight?: number }>;\n}): DegradedTable {\n const edges = params.edges ?? [];\n if (edges.length > 0) {\n return {\n columns: ['Source', 'Target', 'Label'],\n rows: edges.slice(0, MAX_PROJECTED_ROWS).map((e) => {\n const label = [e.weight != null ? String(e.weight) : '', e.label ?? '']\n .filter(Boolean)\n .join(' · ');\n return [cell(e.source), cell(e.target), label];\n }),\n };\n }\n const nodes = params.nodes ?? [];\n return {\n columns: ['Node', 'Label'],\n rows: nodes.slice(0, MAX_PROJECTED_ROWS).map((n) => [cell(n.id), cell(n.label ?? n.id)]),\n };\n}\n\n// ─── Map ───────────────────────────────────────────────────────────────────\n\ninterface GeoJSONLikeFeature {\n geometry?: { type?: string; coordinates?: unknown } | null;\n properties?: Record<string, unknown> | null;\n}\n\n/** Pull a representative `[lng, lat]` pair out of a geometry's coordinates. */\nfunction firstLngLat(coords: unknown): [number, number] | null {\n let c: unknown = coords;\n // Descend nested arrays until we reach a [number, number, ...] position.\n while (Array.isArray(c) && Array.isArray(c[0])) c = c[0];\n if (Array.isArray(c) && typeof c[0] === 'number' && typeof c[1] === 'number') {\n return [c[0], c[1]];\n }\n return null;\n}\n\n/**\n * Map → a coordinate table. Markers become Lat/Lng/Label rows; GeoJSON\n * features become Type / Lat / Lng (+ a compact properties summary). So a\n * map that can't paint still lists where its points are.\n */\nexport function mapToDegradedTable(params: {\n markers?: Array<{\n position: [number, number] | { lat: number; lng: number };\n tooltip?: string;\n popup?: string;\n }>;\n geojson?: unknown;\n}): DegradedTable {\n const rows: Array<Array<string | number>> = [];\n\n for (const m of params.markers ?? []) {\n const lat = Array.isArray(m.position) ? m.position[0] : m.position?.lat;\n const lng = Array.isArray(m.position) ? m.position[1] : m.position?.lng;\n rows.push(['marker', cell(lat), cell(lng), cell(m.tooltip ?? m.popup ?? '')]);\n }\n\n const fc = params.geojson as { features?: GeoJSONLikeFeature[] } | undefined;\n const features = Array.isArray(fc?.features) ? fc!.features : [];\n for (const f of features) {\n const ll = firstLngLat(f.geometry?.coordinates);\n const props = f.properties ?? {};\n const propSummary = Object.keys(props)\n .slice(0, 3)\n .map((k) => `${k}=${cell(props[k])}`)\n .join(', ');\n rows.push([\n cell(f.geometry?.type ?? 'feature'),\n ll ? cell(ll[1]) : '',\n ll ? cell(ll[0]) : '',\n propSummary,\n ]);\n }\n\n return {\n columns: ['Type', 'Lat', 'Lng', 'Info'],\n rows: rows.slice(0, MAX_PROJECTED_ROWS),\n };\n}\n\n// ─── Chart ───────────────────────────────────────────────────────────────\n\n/**\n * Chart → a series table: one row per label, one column per dataset. So a\n * chart that can't draw still shows its numbers. Point/object data (scatter,\n * bubble, time series) is stringified per cell.\n */\nexport function chartToDegradedTable(params: {\n data?: {\n labels?: Array<string | number>;\n datasets?: Array<{ label?: string; data?: unknown[] }>;\n };\n}): DegradedTable {\n const datasets = params.data?.datasets ?? [];\n const labels = params.data?.labels ?? [];\n const rowCount = Math.max(labels.length, ...datasets.map((d) => d.data?.length ?? 0), 0);\n\n const columns = ['', ...datasets.map((d, i) => d.label ?? `Series ${i + 1}`)];\n const rows: Array<Array<string | number>> = [];\n for (let r = 0; r < Math.min(rowCount, MAX_PROJECTED_ROWS); r++) {\n rows.push([cell(labels[r] ?? r + 1), ...datasets.map((d) => cell(d.data?.[r]))]);\n }\n return { columns, rows };\n}\n"],"names":["_a"],"mappings":"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"}
@@ -0,0 +1,64 @@
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
+ export interface DegradedTable {
14
+ columns: string[];
15
+ rows: Array<Array<string | number>>;
16
+ }
17
+ /**
18
+ * Graph → edge table when edges exist (Source / Target / Label), else a
19
+ * node list (Node / Label). The graph renderer can therefore degrade to a
20
+ * readable relationship listing instead of a blank canvas.
21
+ */
22
+ export declare function graphToDegradedTable(params: {
23
+ nodes?: Array<{
24
+ id: string;
25
+ label?: string;
26
+ }>;
27
+ edges?: Array<{
28
+ source: string;
29
+ target: string;
30
+ label?: string;
31
+ weight?: number;
32
+ }>;
33
+ }): DegradedTable;
34
+ /**
35
+ * Map → a coordinate table. Markers become Lat/Lng/Label rows; GeoJSON
36
+ * features become Type / Lat / Lng (+ a compact properties summary). So a
37
+ * map that can't paint still lists where its points are.
38
+ */
39
+ export declare function mapToDegradedTable(params: {
40
+ markers?: Array<{
41
+ position: [number, number] | {
42
+ lat: number;
43
+ lng: number;
44
+ };
45
+ tooltip?: string;
46
+ popup?: string;
47
+ }>;
48
+ geojson?: unknown;
49
+ }): DegradedTable;
50
+ /**
51
+ * Chart → a series table: one row per label, one column per dataset. So a
52
+ * chart that can't draw still shows its numbers. Point/object data (scatter,
53
+ * bubble, time series) is stringified per cell.
54
+ */
55
+ export declare function chartToDegradedTable(params: {
56
+ data?: {
57
+ labels?: Array<string | number>;
58
+ datasets?: Array<{
59
+ label?: string;
60
+ data?: unknown[];
61
+ }>;
62
+ };
63
+ }): DegradedTable;
64
+ //# sourceMappingURL=degraded-projections.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"degraded-projections.d.ts","sourceRoot":"","sources":["../../src/utils/degraded-projections.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC;CACrC;AAmBD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE;IAC3C,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9C,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpF,GAAG,aAAa,CAkBhB;AAoBD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IACzC,OAAO,CAAC,EAAE,KAAK,CAAC;QACd,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAC;QAC1D,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC,CAAC;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,GAAG,aAAa,CA8BhB;AAID;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE;IAC3C,IAAI,CAAC,EAAE;QACL,MAAM,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;QAChC,QAAQ,CAAC,EAAE,KAAK,CAAC;YAAE,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,OAAO,EAAE,CAAA;SAAE,CAAC,CAAC;KACxD,CAAC;CACH,GAAG,aAAa,CAWhB"}
@@ -0,0 +1,87 @@
1
+ const MAX_PROJECTED_ROWS = 200;
2
+ function cell(value) {
3
+ if (value == null) return "";
4
+ if (typeof value === "object") {
5
+ try {
6
+ return JSON.stringify(value);
7
+ } catch {
8
+ return String(value);
9
+ }
10
+ }
11
+ return String(value);
12
+ }
13
+ function graphToDegradedTable(params) {
14
+ const edges = params.edges ?? [];
15
+ if (edges.length > 0) {
16
+ return {
17
+ columns: ["Source", "Target", "Label"],
18
+ rows: edges.slice(0, MAX_PROJECTED_ROWS).map((e) => {
19
+ const label = [e.weight != null ? String(e.weight) : "", e.label ?? ""].filter(Boolean).join(" · ");
20
+ return [cell(e.source), cell(e.target), label];
21
+ })
22
+ };
23
+ }
24
+ const nodes = params.nodes ?? [];
25
+ return {
26
+ columns: ["Node", "Label"],
27
+ rows: nodes.slice(0, MAX_PROJECTED_ROWS).map((n) => [cell(n.id), cell(n.label ?? n.id)])
28
+ };
29
+ }
30
+ function firstLngLat(coords) {
31
+ let c = coords;
32
+ while (Array.isArray(c) && Array.isArray(c[0])) c = c[0];
33
+ if (Array.isArray(c) && typeof c[0] === "number" && typeof c[1] === "number") {
34
+ return [c[0], c[1]];
35
+ }
36
+ return null;
37
+ }
38
+ function mapToDegradedTable(params) {
39
+ var _a, _b, _c, _d;
40
+ const rows = [];
41
+ for (const m of params.markers ?? []) {
42
+ const lat = Array.isArray(m.position) ? m.position[0] : (_a = m.position) == null ? void 0 : _a.lat;
43
+ const lng = Array.isArray(m.position) ? m.position[1] : (_b = m.position) == null ? void 0 : _b.lng;
44
+ rows.push(["marker", cell(lat), cell(lng), cell(m.tooltip ?? m.popup ?? "")]);
45
+ }
46
+ const fc = params.geojson;
47
+ const features = Array.isArray(fc == null ? void 0 : fc.features) ? fc.features : [];
48
+ for (const f of features) {
49
+ const ll = firstLngLat((_c = f.geometry) == null ? void 0 : _c.coordinates);
50
+ const props = f.properties ?? {};
51
+ const propSummary = Object.keys(props).slice(0, 3).map((k) => `${k}=${cell(props[k])}`).join(", ");
52
+ rows.push([
53
+ cell(((_d = f.geometry) == null ? void 0 : _d.type) ?? "feature"),
54
+ ll ? cell(ll[1]) : "",
55
+ ll ? cell(ll[0]) : "",
56
+ propSummary
57
+ ]);
58
+ }
59
+ return {
60
+ columns: ["Type", "Lat", "Lng", "Info"],
61
+ rows: rows.slice(0, MAX_PROJECTED_ROWS)
62
+ };
63
+ }
64
+ function chartToDegradedTable(params) {
65
+ var _a, _b;
66
+ const datasets = ((_a = params.data) == null ? void 0 : _a.datasets) ?? [];
67
+ const labels = ((_b = params.data) == null ? void 0 : _b.labels) ?? [];
68
+ const rowCount = Math.max(labels.length, ...datasets.map((d) => {
69
+ var _a2;
70
+ return ((_a2 = d.data) == null ? void 0 : _a2.length) ?? 0;
71
+ }), 0);
72
+ const columns = ["", ...datasets.map((d, i) => d.label ?? `Series ${i + 1}`)];
73
+ const rows = [];
74
+ for (let r = 0; r < Math.min(rowCount, MAX_PROJECTED_ROWS); r++) {
75
+ rows.push([cell(labels[r] ?? r + 1), ...datasets.map((d) => {
76
+ var _a2;
77
+ return cell((_a2 = d.data) == null ? void 0 : _a2[r]);
78
+ })]);
79
+ }
80
+ return { columns, rows };
81
+ }
82
+ export {
83
+ chartToDegradedTable,
84
+ graphToDegradedTable,
85
+ mapToDegradedTable
86
+ };
87
+ //# sourceMappingURL=degraded-projections.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"degraded-projections.js","sources":["../../src/utils/degraded-projections.ts"],"sourcesContent":["/**\n * Degraded-fallback projections (audit 2026-05-30, P2.5).\n *\n * Pure functions that turn a heavy renderer's params into a flat\n * `{ columns, rows }` table for `<DegradedFallback>` — the middle rung of\n * the fallback ladder shown when the native render (G6 / Leaflet / Chart.js)\n * is unavailable or throws. No peer deps, no side effects, fully testable.\n *\n * These are best-effort views: they surface the underlying data so the user\n * isn't left with a blank space, not faithful reproductions of the chart/\n * map/graph.\n */\n\nexport interface DegradedTable {\n columns: string[];\n rows: Array<Array<string | number>>;\n}\n\nconst MAX_PROJECTED_ROWS = 200;\n\n/** Compact a value to a single table cell string. */\nfunction cell(value: unknown): string {\n if (value == null) return '';\n if (typeof value === 'object') {\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n }\n return String(value);\n}\n\n// ─── Graph ───────────────────────────────────────────────────────────────\n\n/**\n * Graph → edge table when edges exist (Source / Target / Label), else a\n * node list (Node / Label). The graph renderer can therefore degrade to a\n * readable relationship listing instead of a blank canvas.\n */\nexport function graphToDegradedTable(params: {\n nodes?: Array<{ id: string; label?: string }>;\n edges?: Array<{ source: string; target: string; label?: string; weight?: number }>;\n}): DegradedTable {\n const edges = params.edges ?? [];\n if (edges.length > 0) {\n return {\n columns: ['Source', 'Target', 'Label'],\n rows: edges.slice(0, MAX_PROJECTED_ROWS).map((e) => {\n const label = [e.weight != null ? String(e.weight) : '', e.label ?? '']\n .filter(Boolean)\n .join(' · ');\n return [cell(e.source), cell(e.target), label];\n }),\n };\n }\n const nodes = params.nodes ?? [];\n return {\n columns: ['Node', 'Label'],\n rows: nodes.slice(0, MAX_PROJECTED_ROWS).map((n) => [cell(n.id), cell(n.label ?? n.id)]),\n };\n}\n\n// ─── Map ───────────────────────────────────────────────────────────────────\n\ninterface GeoJSONLikeFeature {\n geometry?: { type?: string; coordinates?: unknown } | null;\n properties?: Record<string, unknown> | null;\n}\n\n/** Pull a representative `[lng, lat]` pair out of a geometry's coordinates. */\nfunction firstLngLat(coords: unknown): [number, number] | null {\n let c: unknown = coords;\n // Descend nested arrays until we reach a [number, number, ...] position.\n while (Array.isArray(c) && Array.isArray(c[0])) c = c[0];\n if (Array.isArray(c) && typeof c[0] === 'number' && typeof c[1] === 'number') {\n return [c[0], c[1]];\n }\n return null;\n}\n\n/**\n * Map → a coordinate table. Markers become Lat/Lng/Label rows; GeoJSON\n * features become Type / Lat / Lng (+ a compact properties summary). So a\n * map that can't paint still lists where its points are.\n */\nexport function mapToDegradedTable(params: {\n markers?: Array<{\n position: [number, number] | { lat: number; lng: number };\n tooltip?: string;\n popup?: string;\n }>;\n geojson?: unknown;\n}): DegradedTable {\n const rows: Array<Array<string | number>> = [];\n\n for (const m of params.markers ?? []) {\n const lat = Array.isArray(m.position) ? m.position[0] : m.position?.lat;\n const lng = Array.isArray(m.position) ? m.position[1] : m.position?.lng;\n rows.push(['marker', cell(lat), cell(lng), cell(m.tooltip ?? m.popup ?? '')]);\n }\n\n const fc = params.geojson as { features?: GeoJSONLikeFeature[] } | undefined;\n const features = Array.isArray(fc?.features) ? fc!.features : [];\n for (const f of features) {\n const ll = firstLngLat(f.geometry?.coordinates);\n const props = f.properties ?? {};\n const propSummary = Object.keys(props)\n .slice(0, 3)\n .map((k) => `${k}=${cell(props[k])}`)\n .join(', ');\n rows.push([\n cell(f.geometry?.type ?? 'feature'),\n ll ? cell(ll[1]) : '',\n ll ? cell(ll[0]) : '',\n propSummary,\n ]);\n }\n\n return {\n columns: ['Type', 'Lat', 'Lng', 'Info'],\n rows: rows.slice(0, MAX_PROJECTED_ROWS),\n };\n}\n\n// ─── Chart ───────────────────────────────────────────────────────────────\n\n/**\n * Chart → a series table: one row per label, one column per dataset. So a\n * chart that can't draw still shows its numbers. Point/object data (scatter,\n * bubble, time series) is stringified per cell.\n */\nexport function chartToDegradedTable(params: {\n data?: {\n labels?: Array<string | number>;\n datasets?: Array<{ label?: string; data?: unknown[] }>;\n };\n}): DegradedTable {\n const datasets = params.data?.datasets ?? [];\n const labels = params.data?.labels ?? [];\n const rowCount = Math.max(labels.length, ...datasets.map((d) => d.data?.length ?? 0), 0);\n\n const columns = ['', ...datasets.map((d, i) => d.label ?? `Series ${i + 1}`)];\n const rows: Array<Array<string | number>> = [];\n for (let r = 0; r < Math.min(rowCount, MAX_PROJECTED_ROWS); r++) {\n rows.push([cell(labels[r] ?? r + 1), ...datasets.map((d) => cell(d.data?.[r]))]);\n }\n return { columns, rows };\n}\n"],"names":["_a"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seed-ship/mcp-ui-solid",
3
- "version": "6.8.1",
3
+ "version": "6.9.0",
4
4
  "description": "SolidJS components for rendering MCP-generated UI resources",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -8,41 +8,44 @@
8
8
  * ```
9
9
  */
10
10
 
11
- import { Component, createEffect, onCleanup, createSignal, Show } from 'solid-js'
12
- import type { UIComponent, ChartComponentParams } from '../types'
13
- import { ExpandableWrapper, useExpanded } from './ExpandableWrapper'
11
+ import { Component, createEffect, onCleanup, createSignal, Show } from 'solid-js';
12
+ import type { UIComponent, ChartComponentParams } from '../types';
13
+ import { ExpandableWrapper, useExpanded } from './ExpandableWrapper';
14
+ import { DegradedFallback } from './DegradedFallback';
15
+ import { chartToDegradedTable } from '../utils/degraded-projections';
16
+ import { useTelemetry } from '../context/MCPUITelemetryContext';
14
17
 
15
18
  // Lazy load Chart.js to avoid bundling if not used
16
- let ChartJS: any = null
17
- let chartJSLoadPromise: Promise<any> | null = null
19
+ let ChartJS: any = null;
20
+ let chartJSLoadPromise: Promise<any> | null = null;
18
21
 
19
22
  const loadChartJS = async () => {
20
- if (ChartJS) return ChartJS
23
+ if (ChartJS) return ChartJS;
21
24
 
22
25
  if (!chartJSLoadPromise) {
23
26
  chartJSLoadPromise = import('chart.js/auto')
24
27
  .then((module) => {
25
- ChartJS = module.default || module.Chart
26
- return ChartJS
28
+ ChartJS = module.default || module.Chart;
29
+ return ChartJS;
27
30
  })
28
31
  .catch((err) => {
29
- chartJSLoadPromise = null
30
- throw err
31
- })
32
+ chartJSLoadPromise = null;
33
+ throw err;
34
+ });
32
35
  }
33
36
 
34
- return chartJSLoadPromise
35
- }
37
+ return chartJSLoadPromise;
38
+ };
36
39
 
37
40
  /**
38
41
  * Check if Chart.js is available
39
42
  */
40
43
  export async function isChartJSAvailable(): Promise<boolean> {
41
44
  try {
42
- await loadChartJS()
43
- return true
45
+ await loadChartJS();
46
+ return true;
44
47
  } catch {
45
- return false
48
+ return false;
46
49
  }
47
50
  }
48
51
 
@@ -50,18 +53,18 @@ export interface ChartJSRendererProps {
50
53
  /**
51
54
  * UIComponent with chart params
52
55
  */
53
- component: UIComponent
56
+ component: UIComponent;
54
57
 
55
58
  /**
56
59
  * Error callback
57
60
  */
58
- onError?: (error: Error) => void
61
+ onError?: (error: Error) => void;
59
62
 
60
63
  /**
61
64
  * Forwarded to the underlying `<ExpandableWrapper>` (v6.3.1).
62
65
  * @see ExpandableWrapperProps.toolbarVariant
63
66
  */
64
- toolbarVariant?: 'hover' | 'always-visible'
67
+ toolbarVariant?: 'hover' | 'always-visible';
65
68
  }
66
69
 
67
70
  /**
@@ -87,50 +90,51 @@ export interface ChartJSRendererProps {
87
90
  * ```
88
91
  */
89
92
  export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
90
- const [isLoading, setIsLoading] = createSignal(true)
91
- const [error, setError] = createSignal<string>()
92
- let canvasRef: HTMLCanvasElement | undefined
93
- let chartInstance: any
93
+ const [isLoading, setIsLoading] = createSignal(true);
94
+ const [error, setError] = createSignal<string>();
95
+ let canvasRef: HTMLCanvasElement | undefined;
96
+ let chartInstance: any;
94
97
 
95
- const params = () => props.component.params as ChartComponentParams
96
- const isExpanded = useExpanded()
98
+ const params = () => props.component.params as ChartComponentParams;
99
+ const isExpanded = useExpanded();
100
+ const telemetry = useTelemetry();
97
101
 
98
102
  // v6.1.0 — export visibility :
99
103
  // - undefined / true → button shown (new default, was opt-in)
100
104
  // - false → button hidden (explicit opt-out, unchanged)
101
- const exportEnabled = () => params().exportable !== false
105
+ const exportEnabled = () => params().exportable !== false;
102
106
 
103
107
  // v6.1.0 — copy data for the ExpandableWrapper modal-header copy button.
104
108
  // Lazy-stringified each time the button is clicked.
105
- const copyDataJSON = () => JSON.stringify({ type: params().type, data: params().data }, null, 2)
109
+ const copyDataJSON = () => JSON.stringify({ type: params().type, data: params().data }, null, 2);
106
110
 
107
111
  // Chart PNG export
108
112
  const handleExportPNG = () => {
109
- if (!canvasRef) return
110
- const url = canvasRef.toDataURL('image/png')
111
- const a = document.createElement('a')
112
- a.href = url
113
- a.download = `${(params().title || 'chart').replace(/\s+/g, '-').toLowerCase()}.png`
114
- a.click()
115
- }
113
+ if (!canvasRef) return;
114
+ const url = canvasRef.toDataURL('image/png');
115
+ const a = document.createElement('a');
116
+ a.href = url;
117
+ a.download = `${(params().title || 'chart').replace(/\s+/g, '-').toLowerCase()}.png`;
118
+ a.click();
119
+ };
116
120
 
117
121
  // Create/update chart when params change
118
122
  createEffect(async () => {
119
- if (!canvasRef) return
123
+ if (!canvasRef) return;
120
124
 
121
125
  // Access params to track dependencies
122
- const chartParams = params()
126
+ const chartParams = params();
123
127
 
124
- setIsLoading(true)
125
- setError(undefined)
128
+ setIsLoading(true);
129
+ setError(undefined);
126
130
 
127
131
  try {
128
- const Chart = await loadChartJS()
132
+ const Chart = await loadChartJS();
129
133
 
130
134
  // Destroy previous instance
131
135
  if (chartInstance) {
132
- chartInstance.destroy()
133
- chartInstance = null
136
+ chartInstance.destroy();
137
+ chartInstance = null;
134
138
  }
135
139
 
136
140
  // Build options, merging time-axis config if present (v3.1.0)
@@ -146,11 +150,11 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
146
150
  ...chartParams.options?.plugins?.legend,
147
151
  },
148
152
  },
149
- }
153
+ };
150
154
 
151
155
  // Time-series axis (v3.1.0)
152
156
  if (chartParams.timeAxis) {
153
- const ta = chartParams.timeAxis
157
+ const ta = chartParams.timeAxis;
154
158
  baseOptions.scales = {
155
159
  ...baseOptions.scales,
156
160
  x: {
@@ -164,7 +168,7 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
164
168
  ...(ta.min ? { min: ta.min } : {}),
165
169
  ...(ta.max ? { max: ta.max } : {}),
166
170
  },
167
- }
171
+ };
168
172
  }
169
173
 
170
174
  // Create new chart
@@ -172,24 +176,33 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
172
176
  type: chartParams.type,
173
177
  data: chartParams.data,
174
178
  options: baseOptions,
175
- })
179
+ });
176
180
 
177
- setIsLoading(false)
181
+ setIsLoading(false);
178
182
  } catch (err) {
179
- const error = err instanceof Error ? err : new Error('Chart rendering failed')
180
- setError(error.message)
181
- setIsLoading(false)
182
- props.onError?.(error)
183
+ const error = err instanceof Error ? err : new Error('Chart rendering failed');
184
+ setError(error.message);
185
+ setIsLoading(false);
186
+ // Fallback ladder (P2.5): record the failure so it's observable, then
187
+ // degrade to the series table below instead of a blank canvas.
188
+ telemetry?.dispatch({
189
+ type: 'render:error',
190
+ errorMessage: error.message,
191
+ id: props.component?.id ?? '',
192
+ componentType: 'chart',
193
+ ts: Date.now(),
194
+ });
195
+ props.onError?.(error);
183
196
  }
184
- })
197
+ });
185
198
 
186
199
  // Cleanup on unmount
187
200
  onCleanup(() => {
188
201
  if (chartInstance) {
189
- chartInstance.destroy()
190
- chartInstance = null
202
+ chartInstance.destroy();
203
+ chartInstance = null;
191
204
  }
192
- })
205
+ });
193
206
 
194
207
  return (
195
208
  <ExpandableWrapper
@@ -198,15 +211,15 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
198
211
  copyLabel="Copy chart data (JSON)"
199
212
  toolbarVariant={props.toolbarVariant}
200
213
  >
201
- <div class={`relative w-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden p-4 group ${
202
- isExpanded() ? 'flex-1 min-h-0 flex flex-col' : ''
203
- }`}>
214
+ <div
215
+ class={`relative w-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden p-4 group ${
216
+ isExpanded() ? 'flex-1 min-h-0 flex flex-col' : ''
217
+ }`}
218
+ >
204
219
  <Show when={params().title || exportEnabled()}>
205
220
  <div class="flex items-center justify-between mb-3 flex-shrink-0">
206
221
  <Show when={params().title}>
207
- <h3 class="text-sm font-semibold text-gray-900 dark:text-white">
208
- {params().title}
209
- </h3>
222
+ <h3 class="text-sm font-semibold text-gray-900 dark:text-white">{params().title}</h3>
210
223
  </Show>
211
224
  <Show when={exportEnabled()}>
212
225
  <button
@@ -215,8 +228,18 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
215
228
  title="Download PNG"
216
229
  aria-label="Download chart as PNG"
217
230
  >
218
- <svg class="w-3 h-3 text-gray-500 dark:text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
219
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
231
+ <svg
232
+ class="w-3 h-3 text-gray-500 dark:text-gray-400"
233
+ fill="none"
234
+ viewBox="0 0 24 24"
235
+ stroke="currentColor"
236
+ >
237
+ <path
238
+ stroke-linecap="round"
239
+ stroke-linejoin="round"
240
+ stroke-width="2"
241
+ d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
242
+ />
220
243
  </svg>
221
244
  </button>
222
245
  </Show>
@@ -232,28 +255,14 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
232
255
  </div>
233
256
  </Show>
234
257
 
258
+ {/* Fallback ladder (P2.5): degrade to a series table on render error
259
+ instead of a bare "Chart Error" message. */}
235
260
  <Show when={error()}>
236
- <div class="absolute inset-0 flex items-center justify-center p-4 bg-white dark:bg-gray-800">
237
- <div class="text-center">
238
- <div class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-red-100 dark:bg-red-900/20 mb-3">
239
- <svg
240
- class="w-6 h-6 text-red-600 dark:text-red-400"
241
- fill="none"
242
- viewBox="0 0 24 24"
243
- stroke="currentColor"
244
- >
245
- <path
246
- stroke-linecap="round"
247
- stroke-linejoin="round"
248
- stroke-width="2"
249
- d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
250
- />
251
- </svg>
252
- </div>
253
- <p class="text-red-600 dark:text-red-400 text-sm font-medium">Chart Error</p>
254
- <p class="text-gray-600 dark:text-gray-400 text-xs mt-1 max-w-xs">{error()}</p>
255
- </div>
256
- </div>
261
+ <DegradedFallback
262
+ message={`Chart rendering failed: ${error()}`}
263
+ caption="Showing the chart data as a table the interactive chart is unavailable."
264
+ {...chartToDegradedTable(params() ?? {})}
265
+ />
257
266
  </Show>
258
267
 
259
268
  <div
@@ -270,5 +279,5 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
270
279
  </div>
271
280
  </div>
272
281
  </ExpandableWrapper>
273
- )
274
- }
282
+ );
283
+ };
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Tests for <DegradedFallback> — the middle rung of the renderer fallback
3
+ * ladder (P2.5). Pure presentational component, no peers, no async.
4
+ */
5
+
6
+ import { describe, it, expect } from 'vitest';
7
+ import { render, cleanup } from '@solidjs/testing-library';
8
+ import { DegradedFallback } from './DegradedFallback';
9
+
10
+ describe('<DegradedFallback>', () => {
11
+ it('shows the message and a default caption', () => {
12
+ const { getByText, container } = render(() => (
13
+ <DegradedFallback message="Graph rendering failed" />
14
+ ));
15
+ expect(getByText('Graph rendering failed')).toBeTruthy();
16
+ expect(container.textContent).toContain('interactive view is unavailable');
17
+ cleanup();
18
+ });
19
+
20
+ it('renders a table when columns + rows are provided', () => {
21
+ const { container } = render(() => (
22
+ <DegradedFallback
23
+ message="failed"
24
+ columns={['Source', 'Target', 'Label']}
25
+ rows={[
26
+ ['a', 'b', 'rel'],
27
+ ['b', 'c', ''],
28
+ ]}
29
+ />
30
+ ));
31
+ const headers = container.querySelectorAll('th');
32
+ expect(headers).toHaveLength(3);
33
+ expect(container.querySelectorAll('tbody tr')).toHaveLength(2);
34
+ expect(container.textContent).toContain('rel');
35
+ cleanup();
36
+ });
37
+
38
+ it('shows no table when columns are empty', () => {
39
+ const { container } = render(() => <DegradedFallback message="failed" rows={[['x']]} />);
40
+ expect(container.querySelector('table')).toBeNull();
41
+ cleanup();
42
+ });
43
+
44
+ it('truncates rows past maxRows and notes the remainder', () => {
45
+ const rows = Array.from({ length: 5 }, (_, i) => [String(i)]);
46
+ const { container } = render(() => (
47
+ <DegradedFallback message="failed" columns={['n']} rows={rows} maxRows={2} />
48
+ ));
49
+ expect(container.querySelectorAll('tbody tr')).toHaveLength(2);
50
+ expect(container.textContent).toContain('+3 more rows');
51
+ cleanup();
52
+ });
53
+
54
+ it('uses a custom caption when provided', () => {
55
+ const { container } = render(() => (
56
+ <DegradedFallback message="failed" caption="custom caption here" />
57
+ ));
58
+ expect(container.textContent).toContain('custom caption here');
59
+ cleanup();
60
+ });
61
+ });