@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,93 @@
1
+ /**
2
+ * DegradedFallback — the middle rung of the renderer fallback ladder
3
+ * (audit 2026-05-30, P2.5).
4
+ *
5
+ * Each heavy renderer (graph / map / chart) follows the same contract:
6
+ * 1. native render when its peer lib is available and succeeds;
7
+ * 2. **degraded but useful** view when the native render throws — this
8
+ * component: a visible notice + a plain data table so the user still
9
+ * sees the underlying data instead of a blank space;
10
+ * 3. (the caller also emits a `component:error` telemetry event).
11
+ *
12
+ * Pure / presentational — no peer deps, no side effects — so a render-path
13
+ * failure in a heavy lib can never cascade into the fallback itself, and it
14
+ * is trivially unit-testable. Rows/cells are rendered as text; callers are
15
+ * responsible for stringifying complex cell values.
16
+ */
17
+
18
+ import { Component, For, Show } from 'solid-js';
19
+
20
+ export interface DegradedFallbackProps {
21
+ /** Short, human-readable reason the native render was skipped/failed. */
22
+ message: string;
23
+ /**
24
+ * Column headers for the degraded data table. When omitted (or empty),
25
+ * only the notice banner is shown.
26
+ */
27
+ columns?: string[];
28
+ /** Row data — each row is an array of cells aligned to `columns`. */
29
+ rows?: Array<Array<string | number>>;
30
+ /**
31
+ * Caption under the table. Defaults to a generic
32
+ * "interactive view unavailable" line.
33
+ */
34
+ caption?: string;
35
+ /** Max rows to render before truncating (default 50). */
36
+ maxRows?: number;
37
+ }
38
+
39
+ export const DegradedFallback: Component<DegradedFallbackProps> = (props) => {
40
+ const maxRows = () => props.maxRows ?? 50;
41
+ const allRows = () => props.rows ?? [];
42
+ const shownRows = () => allRows().slice(0, maxRows());
43
+ const hiddenCount = () => Math.max(0, allRows().length - shownRows().length);
44
+ const hasTable = () => (props.columns?.length ?? 0) > 0 && allRows().length > 0;
45
+
46
+ return (
47
+ <div
48
+ class="w-full rounded-lg border border-amber-200 bg-amber-50 p-3 dark:border-amber-800 dark:bg-amber-900/20"
49
+ role="alert"
50
+ >
51
+ <p class="text-sm font-medium text-amber-900 dark:text-amber-100">{props.message}</p>
52
+ <p class="mt-0.5 text-xs text-amber-700 dark:text-amber-300">
53
+ {props.caption ?? 'Showing the underlying data — the interactive view is unavailable.'}
54
+ </p>
55
+
56
+ <Show when={hasTable()}>
57
+ <div class="mt-2 max-h-64 overflow-auto rounded border border-amber-200 dark:border-amber-800">
58
+ <table class="w-full border-collapse text-left text-xs">
59
+ <thead class="sticky top-0 bg-amber-100 dark:bg-amber-900/40">
60
+ <tr>
61
+ <For each={props.columns}>
62
+ {(col) => (
63
+ <th class="px-2 py-1 font-medium text-amber-900 dark:text-amber-100">{col}</th>
64
+ )}
65
+ </For>
66
+ </tr>
67
+ </thead>
68
+ <tbody>
69
+ <For each={shownRows()}>
70
+ {(row) => (
71
+ <tr class="border-t border-amber-100 dark:border-amber-800/60">
72
+ <For each={props.columns}>
73
+ {(_col, i) => (
74
+ <td class="px-2 py-1 text-amber-800 dark:text-amber-200">
75
+ {String(row[i()] ?? '')}
76
+ </td>
77
+ )}
78
+ </For>
79
+ </tr>
80
+ )}
81
+ </For>
82
+ </tbody>
83
+ </table>
84
+ </div>
85
+ <Show when={hiddenCount() > 0}>
86
+ <p class="mt-1 text-[10px] text-amber-600 dark:text-amber-400">
87
+ +{hiddenCount()} more rows not shown.
88
+ </p>
89
+ </Show>
90
+ </Show>
91
+ </div>
92
+ );
93
+ };
@@ -28,6 +28,9 @@ import type {
28
28
  } from '@seed-ship/mcp-ui-spec';
29
29
  import { ExpandableWrapper, useExpanded } from './ExpandableWrapper';
30
30
  import { PortalDropdownMenu } from './PortalDropdownMenu';
31
+ import { DegradedFallback } from './DegradedFallback';
32
+ import { graphToDegradedTable } from '../utils/degraded-projections';
33
+ import { useTelemetry } from '../context/MCPUITelemetryContext';
31
34
 
32
35
  // Module-scoped lazy import promise — first call triggers the dynamic
33
36
  // import, subsequent calls reuse the resolved module.
@@ -220,6 +223,7 @@ export interface GraphRendererProps {
220
223
  export const GraphRenderer: Component<GraphRendererProps> = (props) => {
221
224
  const params = () => props.component.params as GraphComponentParams;
222
225
  const isExpanded = useExpanded();
226
+ const telemetry = useTelemetry();
223
227
  const [available, setAvailable] = createSignal<boolean | null>(null);
224
228
  const [error, setError] = createSignal<string | undefined>();
225
229
  const [exportMenuOpen, setExportMenuOpen] = createSignal(false);
@@ -293,7 +297,18 @@ export const GraphRenderer: Component<GraphRendererProps> = (props) => {
293
297
  graphInstance = new (Graph as any)(config);
294
298
  await graphInstance.render();
295
299
  } catch (err) {
296
- setError(err instanceof Error ? err.message : 'Failed to render graph');
300
+ const message = err instanceof Error ? err.message : 'Failed to render graph';
301
+ setError(message);
302
+ // Fallback ladder (P2.5): the native G6 render threw — emit telemetry
303
+ // so the failure is observable, then degrade to the edge/node table
304
+ // below instead of leaving a blank canvas.
305
+ telemetry?.dispatch({
306
+ type: 'render:error',
307
+ errorMessage: message,
308
+ id: props.component.id ?? '',
309
+ componentType: 'graph',
310
+ ts: Date.now(),
311
+ });
297
312
  }
298
313
  });
299
314
 
@@ -424,19 +439,26 @@ export const GraphRenderer: Component<GraphRendererProps> = (props) => {
424
439
  </PortalDropdownMenu>
425
440
  </div>
426
441
 
442
+ {/* Native G6 canvas — hidden once a render error degrades us. */}
427
443
  <div
428
444
  ref={containerRef}
429
445
  class={`bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden ${
430
- isExpanded() ? 'flex-1 min-h-0' : ''
431
- }`}
446
+ error() ? 'hidden' : ''
447
+ } ${isExpanded() ? 'flex-1 min-h-0' : ''}`}
432
448
  style={
433
449
  isExpanded()
434
450
  ? `height: 100%; width: ${params().width ?? '100%'};`
435
451
  : `height: ${params().height ?? '400px'}; width: ${params().width ?? '100%'};`
436
452
  }
437
453
  />
454
+ {/* Fallback ladder (P2.5): degrade to an edge/node table on error
455
+ rather than showing a bare message. Export menu stays usable. */}
438
456
  <Show when={error()}>
439
- <p class="text-xs text-red-600 dark:text-red-400 mt-1">Render error: {error()}</p>
457
+ <DegradedFallback
458
+ message={`Graph rendering failed: ${error()}`}
459
+ caption="Showing the graph data as a table — the interactive view is unavailable."
460
+ {...graphToDegradedTable(params())}
461
+ />
440
462
  </Show>
441
463
  </div>
442
464
  </ExpandableWrapper>