@seed-ship/mcp-ui-solid 6.17.0 → 6.18.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 (156) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +50 -3
  3. package/dist/adapters/index.d.ts +2 -0
  4. package/dist/adapters/index.d.ts.map +1 -1
  5. package/dist/adapters/presentation.cjs +141 -0
  6. package/dist/adapters/presentation.cjs.map +1 -0
  7. package/dist/adapters/presentation.d.ts +68 -0
  8. package/dist/adapters/presentation.d.ts.map +1 -0
  9. package/dist/adapters/presentation.js +141 -0
  10. package/dist/adapters/presentation.js.map +1 -0
  11. package/dist/adapters.cjs +4 -0
  12. package/dist/adapters.cjs.map +1 -1
  13. package/dist/adapters.d.cts +2 -0
  14. package/dist/adapters.d.ts +2 -0
  15. package/dist/adapters.js +4 -0
  16. package/dist/adapters.js.map +1 -1
  17. package/dist/components/ChartJSRenderer.cjs +165 -65
  18. package/dist/components/ChartJSRenderer.cjs.map +1 -1
  19. package/dist/components/ChartJSRenderer.d.ts.map +1 -1
  20. package/dist/components/ChartJSRenderer.js +168 -68
  21. package/dist/components/ChartJSRenderer.js.map +1 -1
  22. package/dist/components/ComponentToolbar.cjs.map +1 -1
  23. package/dist/components/ComponentToolbar.d.ts.map +1 -1
  24. package/dist/components/ComponentToolbar.js.map +1 -1
  25. package/dist/components/EditableLayoutContext.cjs +17 -0
  26. package/dist/components/EditableLayoutContext.cjs.map +1 -0
  27. package/dist/components/EditableLayoutContext.d.ts +5 -0
  28. package/dist/components/EditableLayoutContext.d.ts.map +1 -0
  29. package/dist/components/EditableLayoutContext.js +17 -0
  30. package/dist/components/EditableLayoutContext.js.map +1 -0
  31. package/dist/components/EditableUIResourceRenderer.cjs +87 -84
  32. package/dist/components/EditableUIResourceRenderer.cjs.map +1 -1
  33. package/dist/components/EditableUIResourceRenderer.d.ts.map +1 -1
  34. package/dist/components/EditableUIResourceRenderer.js +87 -84
  35. package/dist/components/EditableUIResourceRenderer.js.map +1 -1
  36. package/dist/components/ElicitationForm.cjs.map +1 -1
  37. package/dist/components/ElicitationForm.js.map +1 -1
  38. package/dist/components/FeedbackInline.cjs +0 -1
  39. package/dist/components/FeedbackInline.cjs.map +1 -1
  40. package/dist/components/FeedbackInline.js +0 -1
  41. package/dist/components/FeedbackInline.js.map +1 -1
  42. package/dist/components/GraphRenderer.cjs.map +1 -1
  43. package/dist/components/GraphRenderer.d.ts.map +1 -1
  44. package/dist/components/GraphRenderer.js.map +1 -1
  45. package/dist/components/GridRenderer.cjs +12 -6
  46. package/dist/components/GridRenderer.cjs.map +1 -1
  47. package/dist/components/GridRenderer.d.ts.map +1 -1
  48. package/dist/components/GridRenderer.js +13 -7
  49. package/dist/components/GridRenderer.js.map +1 -1
  50. package/dist/components/MapRenderer.cjs.map +1 -1
  51. package/dist/components/MapRenderer.d.ts.map +1 -1
  52. package/dist/components/MapRenderer.js.map +1 -1
  53. package/dist/components/PresentationFeedback.cjs +0 -1
  54. package/dist/components/PresentationFeedback.cjs.map +1 -1
  55. package/dist/components/PresentationFeedback.js +0 -1
  56. package/dist/components/PresentationFeedback.js.map +1 -1
  57. package/dist/components/UIResourceRenderer.cjs +37 -22
  58. package/dist/components/UIResourceRenderer.cjs.map +1 -1
  59. package/dist/components/UIResourceRenderer.d.ts.map +1 -1
  60. package/dist/components/UIResourceRenderer.js +38 -23
  61. package/dist/components/UIResourceRenderer.js.map +1 -1
  62. package/dist/components/VerifiedText.cjs +0 -4
  63. package/dist/components/VerifiedText.cjs.map +1 -1
  64. package/dist/components/VerifiedText.d.ts.map +1 -1
  65. package/dist/components/VerifiedText.js +0 -4
  66. package/dist/components/VerifiedText.js.map +1 -1
  67. package/dist/components/chart-data-table.cjs +73 -0
  68. package/dist/components/chart-data-table.cjs.map +1 -0
  69. package/dist/components/chart-data-table.d.ts +23 -0
  70. package/dist/components/chart-data-table.d.ts.map +1 -0
  71. package/dist/components/chart-data-table.js +73 -0
  72. package/dist/components/chart-data-table.js.map +1 -0
  73. package/dist/context/MCPUIStringsContext.cjs +10 -9
  74. package/dist/context/MCPUIStringsContext.cjs.map +1 -1
  75. package/dist/context/MCPUIStringsContext.d.ts +17 -5
  76. package/dist/context/MCPUIStringsContext.d.ts.map +1 -1
  77. package/dist/context/MCPUIStringsContext.js +11 -10
  78. package/dist/context/MCPUIStringsContext.js.map +1 -1
  79. package/dist/hooks/createResponsiveGrid.cjs +37 -0
  80. package/dist/hooks/createResponsiveGrid.cjs.map +1 -0
  81. package/dist/hooks/createResponsiveGrid.d.ts +6 -0
  82. package/dist/hooks/createResponsiveGrid.d.ts.map +1 -0
  83. package/dist/hooks/createResponsiveGrid.js +37 -0
  84. package/dist/hooks/createResponsiveGrid.js.map +1 -0
  85. package/dist/index.cjs +1 -0
  86. package/dist/index.cjs.map +1 -1
  87. package/dist/index.js +1 -0
  88. package/dist/index.js.map +1 -1
  89. package/dist/mcp-ui-spec/dist/schemas.cjs +13 -1
  90. package/dist/mcp-ui-spec/dist/schemas.cjs.map +1 -1
  91. package/dist/mcp-ui-spec/dist/schemas.js +13 -1
  92. package/dist/mcp-ui-spec/dist/schemas.js.map +1 -1
  93. package/dist/services/chat-bus.cjs.map +1 -1
  94. package/dist/services/chat-bus.d.ts.map +1 -1
  95. package/dist/services/chat-bus.js.map +1 -1
  96. package/dist/services/chat-prompt-controller.cjs.map +1 -1
  97. package/dist/services/chat-prompt-controller.d.ts.map +1 -1
  98. package/dist/services/chat-prompt-controller.js.map +1 -1
  99. package/dist/services/component-registry.cjs +162 -22
  100. package/dist/services/component-registry.cjs.map +1 -1
  101. package/dist/services/component-registry.d.ts +6 -3
  102. package/dist/services/component-registry.d.ts.map +1 -1
  103. package/dist/services/component-registry.js +162 -22
  104. package/dist/services/component-registry.js.map +1 -1
  105. package/dist/services/telemetry.cjs +0 -1
  106. package/dist/services/telemetry.cjs.map +1 -1
  107. package/dist/services/telemetry.js +0 -1
  108. package/dist/services/telemetry.js.map +1 -1
  109. package/dist/services/validation.cjs +21 -7
  110. package/dist/services/validation.cjs.map +1 -1
  111. package/dist/services/validation.d.ts.map +1 -1
  112. package/dist/services/validation.js +21 -7
  113. package/dist/services/validation.js.map +1 -1
  114. package/dist/types/index.d.ts +43 -8
  115. package/dist/types/index.d.ts.map +1 -1
  116. package/dist/types.d.cts +43 -8
  117. package/dist/types.d.ts +43 -8
  118. package/docs/briefs/VISUALIZATION-FOUNDATION-2026-09-14.md +105 -0
  119. package/package.json +4 -4
  120. package/src/adapters/index.ts +18 -0
  121. package/src/adapters/presentation.test.ts +114 -0
  122. package/src/adapters/presentation.ts +229 -0
  123. package/src/components/ChartJSRenderer.test.tsx +325 -0
  124. package/src/components/ChartJSRenderer.tsx +162 -27
  125. package/src/components/ChatPrompt.test.tsx +1 -1
  126. package/src/components/ComponentToolbar.tsx +1 -1
  127. package/src/components/EditableLayoutContext.tsx +12 -0
  128. package/src/components/EditableUIResourceRenderer.tsx +11 -8
  129. package/src/components/ElicitationForm.tsx +1 -1
  130. package/src/components/FeedbackInline.tsx +1 -1
  131. package/src/components/GraphRenderer.test.tsx +1 -2
  132. package/src/components/GraphRenderer.tsx +0 -2
  133. package/src/components/GridRenderer.tsx +6 -4
  134. package/src/components/MapRenderer.tsx +0 -3
  135. package/src/components/PresentationFeedback.tsx +1 -1
  136. package/src/components/ResponsiveGrid.test.tsx +166 -0
  137. package/src/components/TableRenderer.pagination.test.tsx +31 -0
  138. package/src/components/UIResourceRenderer.tsx +14 -9
  139. package/src/components/VerifiedText.tsx +0 -5
  140. package/src/components/chart-data-table.ts +97 -0
  141. package/src/context/MCPUIStringsContext.test.tsx +60 -0
  142. package/src/context/MCPUIStringsContext.tsx +31 -12
  143. package/src/hooks/createResponsiveGrid.ts +41 -0
  144. package/src/services/chat-bus.test.ts +1 -1
  145. package/src/services/chat-bus.ts +0 -2
  146. package/src/services/chat-prompt-controller.ts +0 -8
  147. package/src/services/component-registry.test.ts +81 -2
  148. package/src/services/component-registry.ts +167 -23
  149. package/src/services/telemetry.ts +1 -1
  150. package/src/services/validation.test.ts +1 -14
  151. package/src/services/validation.ts +23 -6
  152. package/src/services/visualization-validation.test.ts +42 -0
  153. package/src/types/index.ts +50 -9
  154. package/src/types/visualization-contracts.test.ts +66 -0
  155. package/src/utils/perf.test.ts +3 -3
  156. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,31 @@
1
+ import { afterEach, describe, expect, it } from 'vitest'
2
+ import { cleanup, fireEvent, render } from '@solidjs/testing-library'
3
+ import { createSignal } from 'solid-js'
4
+ import { UIResourceRenderer } from './UIResourceRenderer'
5
+ import type { UIComponent } from '../types'
6
+
7
+ afterEach(cleanup)
8
+
9
+ describe('table page bounds', () => {
10
+ it('clamps an oversized initial page and remains navigable after data shrinks', () => {
11
+ const [count, setCount] = createSignal(30)
12
+ const component: UIComponent = {
13
+ id: 'paged-table', type: 'table', position: { colStart: 1, colSpan: 12 },
14
+ params: {
15
+ columns: [{ key: 'name', label: 'Name' }],
16
+ get rows() { return Array.from({ length: count() }, (_, i) => ({ name: `row-${i}` })) },
17
+ initialPage: 99, chatPageSize: 10, className: 'custom-table',
18
+ },
19
+ }
20
+ const { container, getByText } = render(() => <UIResourceRenderer content={component} />)
21
+ expect(container.querySelector('.custom-table')).not.toBeNull()
22
+ expect(container.querySelector('tbody')?.textContent).toContain('row-29')
23
+ expect(getByText('3 / 3')).toBeTruthy()
24
+ setCount(15)
25
+ expect(container.querySelector('tbody')?.textContent).toContain('row-14')
26
+ expect(getByText('2 / 2')).toBeTruthy()
27
+ fireEvent.click(getByText('◀'))
28
+ expect(getByText('1 / 2')).toBeTruthy()
29
+ expect(container.querySelector('tbody')?.textContent).toContain('row-0')
30
+ })
31
+ })
@@ -54,6 +54,7 @@ import { ExpandableWrapper, useExpanded } from './ExpandableWrapper'
54
54
  import { PortalDropdownMenu } from './PortalDropdownMenu'
55
55
  import { RenderProvider } from './RenderContext'
56
56
  import { useAction } from '../hooks/useAction'
57
+ import { createResponsiveGrid } from '../hooks/createResponsiveGrid'
57
58
  import { marked } from 'marked'
58
59
 
59
60
  /**
@@ -711,8 +712,12 @@ function TableRenderer(props: {
711
712
  const hasServerPagination = () => !!tableParams.pagination
712
713
  const needsClientPagination = () =>
713
714
  !hasServerPagination() && !showAll() && clientPageSize() > 0 && filteredRows().length > clientPageSize()
714
- const [clientPage, setClientPage] = createSignal(tableParams.initialPage ?? 0)
715
+ const [requestedClientPage, setClientPage] = createSignal(tableParams.initialPage ?? 0)
715
716
  const clientTotalPages = () => needsClientPagination() ? Math.ceil(filteredRows().length / clientPageSize()) : 1
717
+ const clientPage = createMemo(() => Math.max(0, Math.min(requestedClientPage(), clientTotalPages() - 1)))
718
+ createEffect(() => {
719
+ if (requestedClientPage() !== clientPage()) setClientPage(clientPage())
720
+ })
716
721
  const clientVisibleRows = createMemo(() => {
717
722
  if (showAll() || !needsClientPagination()) return filteredRows()
718
723
  const start = clientPage() * clientPageSize()
@@ -939,7 +944,7 @@ function TableRenderer(props: {
939
944
  <ExpandableWrapper title={tableParams.title || 'Table'} copyData={getTableCSV()} copyLabel="Copy table (CSV)" toolbarVariant={props.toolbarVariant}>
940
945
  <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 group ${
941
946
  isExpanded() ? 'flex-1 min-h-0 flex flex-col' : 'h-full'
942
- }`}>
947
+ } ${tableParams.className || ''}`}>
943
948
  <Show when={exportable} fallback={<CopyButton getText={getTableCSV} title="Copy table (CSV)" position="top-right" />}>
944
949
  <div class="absolute right-10 top-2 z-10">
945
950
  <button
@@ -1866,16 +1871,17 @@ export const UIResourceRenderer: Component<UIResourceRendererProps> = (props) =>
1866
1871
  }
1867
1872
 
1868
1873
  // Convert grid styles to CSS string
1874
+ const responsiveGrid = createResponsiveGrid()
1869
1875
  const gridContainerStyle = () => {
1870
1876
  const layoutData = layout()
1871
- return `grid-template-columns: repeat(${layoutData.grid.columns}, 1fr); gap: ${layoutData.grid.gap}`
1877
+ return `display: grid; grid-template-columns: repeat(${responsiveGrid.stacked() ? 1 : layoutData.grid.columns}, minmax(0, 1fr)); gap: ${layoutData.grid.gap}`
1872
1878
  }
1873
1879
 
1874
1880
  // Convert component grid styles to CSS string
1875
1881
  const getGridStyleString = (component: UIComponent) => {
1876
1882
  // Defensive check for position field - default to full width
1877
- if (!component.position) {
1878
- return 'grid-column: 1 / span 12; grid-row: auto'
1883
+ if (responsiveGrid.stacked() || !component.position) {
1884
+ return 'grid-column: 1 / -1; grid-row: auto'
1879
1885
  }
1880
1886
  const { colStart, colSpan, rowStart, rowSpan = 1 } = component.position
1881
1887
  return `grid-column: ${colStart} / span ${colSpan}; grid-row: ${rowStart ? `${rowStart} / span ${rowSpan}` : 'auto'}`
@@ -1921,8 +1927,6 @@ export const UIResourceRenderer: Component<UIResourceRendererProps> = (props) =>
1921
1927
  }
1922
1928
  })
1923
1929
 
1924
- const layoutData = layout()
1925
-
1926
1930
  // ── Identity + duplicate-mount detection (v6.5.0) ─────────────
1927
1931
  // `isLayoutContent` distinguishes a real composite/layout payload from
1928
1932
  // the synthetic single-component wrapping above. Drives whether the
@@ -1960,10 +1964,11 @@ export const UIResourceRenderer: Component<UIResourceRendererProps> = (props) =>
1960
1964
  ? { 'data-mcp-ui-layout-id': outerKey() }
1961
1965
  : { 'data-mcp-ui-component-id': outerKey() })}
1962
1966
  >
1963
- <div class="grid gap-4" style={gridContainerStyle()}>
1964
- <For each={layoutData.components}>
1967
+ <div ref={responsiveGrid.ref} class="grid gap-4" style={gridContainerStyle()} data-mcp-ui-grid data-mcp-ui-stacked={responsiveGrid.stacked()}>
1968
+ <For each={layout().components}>
1965
1969
  {(component) => (
1966
1970
  <div
1971
+ class="min-w-0"
1967
1972
  style={getGridStyleString(component)}
1968
1973
  data-mcp-ui-component-id={getUiResourceStableKey(component)}
1969
1974
  >
@@ -46,11 +46,6 @@ function buildAnnotatedSegments(text: string, validation: DataValidation): TextS
46
46
  }
47
47
  }
48
48
 
49
- // Build all number positions sorted
50
- const allPositions = validation.llmNumbers
51
- .map(n => ({ position: n.position, length: n.context.length - 20 })) // approximate original match length
52
- .sort((a, b) => a.position - b.position)
53
-
54
49
  // Re-extract number lengths from text for precise splitting
55
50
  const numberRegex = /\d[\d\s,.]*\d|\d+/g
56
51
  const matches: Array<{ start: number; end: number }> = []
@@ -0,0 +1,97 @@
1
+ export interface ChartDataTable {
2
+ columns: string[];
3
+ rows: Array<Array<string | number>>;
4
+ }
5
+
6
+ interface ChartDatasetLike {
7
+ label?: string;
8
+ data?: unknown[];
9
+ }
10
+
11
+ export interface ChartDataLike {
12
+ type?: string;
13
+ data?: {
14
+ labels?: Array<string | number>;
15
+ datasets?: ChartDatasetLike[];
16
+ };
17
+ }
18
+
19
+ function tableCell(value: unknown): string | number {
20
+ if (value == null) return '';
21
+ if (typeof value === 'string' || typeof value === 'number') return value;
22
+ if (typeof value === 'boolean') return String(value);
23
+ try {
24
+ return JSON.stringify(value);
25
+ } catch {
26
+ return String(value);
27
+ }
28
+ }
29
+
30
+ function isPoint(value: unknown): value is Record<string, unknown> {
31
+ return (
32
+ value !== null &&
33
+ typeof value === 'object' &&
34
+ !Array.isArray(value) &&
35
+ ('x' in value || 'y' in value || 'r' in value)
36
+ );
37
+ }
38
+
39
+ /**
40
+ * Project Chart.js data into an accessible table without flattening point
41
+ * coordinates. Categorical charts use a compact wide table; scatter, bubble,
42
+ * and object-valued line charts use one row per point so x/y/r remain distinct.
43
+ */
44
+ export function chartToDataTable(params: ChartDataLike): ChartDataTable {
45
+ const datasets = params.data?.datasets ?? [];
46
+ const labels = params.data?.labels ?? [];
47
+ const pointData =
48
+ params.type === 'scatter' ||
49
+ params.type === 'bubble' ||
50
+ datasets.some((dataset) => dataset.data?.some(isPoint));
51
+
52
+ if (pointData) {
53
+ const hasLabels = labels.length > 0;
54
+ const hasRadius =
55
+ params.type === 'bubble' ||
56
+ datasets.some((dataset) => dataset.data?.some((value) => isPoint(value) && 'r' in value));
57
+ const columns = [
58
+ 'Series',
59
+ 'Point',
60
+ ...(hasLabels ? ['Label'] : []),
61
+ 'x',
62
+ 'y',
63
+ ...(hasRadius ? ['r'] : []),
64
+ ];
65
+ const rows: Array<Array<string | number>> = [];
66
+
67
+ datasets.forEach((dataset, datasetIndex) => {
68
+ const series = dataset.label || `Series ${datasetIndex + 1}`;
69
+ (dataset.data ?? []).forEach((value, pointIndex) => {
70
+ const point = isPoint(value) ? value : { y: value };
71
+ rows.push([
72
+ series,
73
+ pointIndex + 1,
74
+ ...(hasLabels ? [tableCell(labels[pointIndex])] : []),
75
+ tableCell(point.x),
76
+ tableCell(point.y),
77
+ ...(hasRadius ? [tableCell(point.r)] : []),
78
+ ]);
79
+ });
80
+ });
81
+
82
+ return { columns, rows };
83
+ }
84
+
85
+ const rowCount = Math.max(labels.length, ...datasets.map((dataset) => dataset.data?.length ?? 0), 0);
86
+ const columns = ['Label', ...datasets.map((dataset, index) => dataset.label || `Series ${index + 1}`)];
87
+ const rows: Array<Array<string | number>> = [];
88
+
89
+ for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) {
90
+ rows.push([
91
+ tableCell(labels[rowIndex] ?? rowIndex + 1),
92
+ ...datasets.map((dataset) => tableCell(dataset.data?.[rowIndex])),
93
+ ]);
94
+ }
95
+
96
+ return { columns, rows };
97
+ }
@@ -11,10 +11,13 @@
11
11
 
12
12
  import { describe, it, expect, beforeEach } from 'vitest'
13
13
  import { render, cleanup, fireEvent } from '@solidjs/testing-library'
14
+ import { createSignal } from 'solid-js'
14
15
  import {
16
+ MCPUIStringsContext,
15
17
  MCPUIStringsProvider,
16
18
  useMCPUIStrings,
17
19
  DEFAULT_MCPUI_STRINGS,
20
+ type MCPUIStrings,
18
21
  } from './MCPUIStringsContext'
19
22
  import { FeedbackInline } from '../components/FeedbackInline'
20
23
  import { ExpandableWrapper } from '../components/ExpandableWrapper'
@@ -27,6 +30,8 @@ describe('MCPUIStringsContext (v6.6.0)', () => {
27
30
  expect(DEFAULT_MCPUI_STRINGS.feedbackUseful).toBe('Useful')
28
31
  expect(DEFAULT_MCPUI_STRINGS.feedbackPositiveAck).toBe('Thanks!')
29
32
  expect(DEFAULT_MCPUI_STRINGS.retry).toBe('Retry')
33
+ expect(DEFAULT_MCPUI_STRINGS.chartView).toBe('Chart')
34
+ expect(DEFAULT_MCPUI_STRINGS.chartDataView).toBe('Data')
30
35
  })
31
36
 
32
37
  it('useMCPUIStrings returns the EN defaults with no provider mounted', () => {
@@ -58,6 +63,61 @@ describe('MCPUIStringsContext (v6.6.0)', () => {
58
63
  expect(captured!.closeExpandedView).toBe('Close expanded view')
59
64
  })
60
65
 
66
+ it('accepts a legacy typed dictionary and resolves chart defaults through the direct context', () => {
67
+ const legacyStrings: MCPUIStrings = {
68
+ expand: 'Legacy expand',
69
+ expandedView: 'Legacy expanded view',
70
+ copyToClipboard: 'Legacy copy',
71
+ closeExpandedView: 'Legacy close',
72
+ feedbackUseful: 'Legacy useful',
73
+ feedbackNotUseful: 'Legacy not useful',
74
+ feedbackPositiveAck: 'Legacy positive',
75
+ feedbackNegativeAck: 'Legacy negative',
76
+ retry: 'Legacy retry',
77
+ }
78
+ let captured: ReturnType<typeof useMCPUIStrings> | undefined
79
+ const Probe = () => {
80
+ captured = useMCPUIStrings()
81
+ return <span>probe</span>
82
+ }
83
+
84
+ render(() => (
85
+ <MCPUIStringsContext.Provider value={legacyStrings}>
86
+ <Probe />
87
+ </MCPUIStringsContext.Provider>
88
+ ))
89
+
90
+ expect(captured!.expand).toBe('Legacy expand')
91
+ expect(captured!.chartView).toBe('Chart')
92
+ expect(captured!.chartDataView).toBe('Data')
93
+ expect(captured!.chartViewSelector).toBe('Chart or data view')
94
+ })
95
+
96
+ it('keeps replacement provider overrides reactive while resolving omitted defaults', () => {
97
+ const [overrides, setOverrides] = createSignal<Partial<MCPUIStrings>>({
98
+ chartView: 'Diagram',
99
+ })
100
+ const Probe = () => {
101
+ const strings = useMCPUIStrings()
102
+ return (
103
+ <button type="button" onClick={() => setOverrides({ chartView: 'Graphique' })}>
104
+ {strings.chartView}|{strings.chartDataView}
105
+ </button>
106
+ )
107
+ }
108
+
109
+ const { getByRole } = render(() => (
110
+ <MCPUIStringsProvider strings={overrides()}>
111
+ <Probe />
112
+ </MCPUIStringsProvider>
113
+ ))
114
+ const button = getByRole('button')
115
+
116
+ expect(button.textContent).toBe('Diagram|Data')
117
+ fireEvent.click(button)
118
+ expect(button.textContent).toBe('Graphique|Data')
119
+ })
120
+
61
121
  it('FeedbackInline reads its ack from the provider (FR override)', () => {
62
122
  const { getByText, container } = render(() => (
63
123
  <MCPUIStringsProvider
@@ -39,14 +39,14 @@
39
39
  * ```
40
40
  */
41
41
 
42
- import { createContext, useContext, type JSX } from 'solid-js'
42
+ import { createContext, mergeProps, useContext, type JSX } from 'solid-js'
43
43
 
44
44
  /**
45
45
  * The library's own chrome strings. Flat map, no interpolation.
46
46
  *
47
- * Marked exhaustive as of v6.6.0 ; new chrome strings added by later
48
- * renderers extend this interface (the `MCPUIStringsProvider` merge keeps
49
- * older consumers working — any unset key falls back to the EN default).
47
+ * The original chrome keys remain required. Later additions are optional
48
+ * so existing typed dictionaries remain valid across minor releases.
49
+ * `useMCPUIStrings` resolves omitted keys to their English defaults.
50
50
  */
51
51
  export interface MCPUIStrings {
52
52
  // ── ExpandableWrapper toolbar ──────────────────────────────
@@ -72,13 +72,27 @@ export interface MCPUIStrings {
72
72
  // ── Generic chrome ────────────────────────────────────────
73
73
  /** Label of the streaming retry button. */
74
74
  retry: string
75
+
76
+ // ── Chart data access ─────────────────────────────────────
77
+ /** Label of the native chart view button. */
78
+ chartView?: string
79
+ /** Label of the exact-data table view button. */
80
+ chartDataView?: string
81
+ /** Accessible name of the chart/data view selector. */
82
+ chartViewSelector?: string
83
+ /** Caption and accessible name for the chart's data table. */
84
+ chartDataTable?: string
85
+ /** Screen-reader description associated with the chart canvas. */
86
+ chartDataSummary?: string
87
+ /** Empty state shown when a chart has no data points. */
88
+ chartNoData?: string
75
89
  }
76
90
 
77
91
  /**
78
92
  * English defaults. A published library ships no hardcoded non-English
79
93
  * chrome — consumers localize via `<MCPUIStringsProvider>`.
80
94
  */
81
- export const DEFAULT_MCPUI_STRINGS: MCPUIStrings = {
95
+ export const DEFAULT_MCPUI_STRINGS: Required<MCPUIStrings> = {
82
96
  expand: 'Expand',
83
97
  expandedView: 'Expanded view',
84
98
  copyToClipboard: 'Copy to clipboard',
@@ -88,6 +102,12 @@ export const DEFAULT_MCPUI_STRINGS: MCPUIStrings = {
88
102
  feedbackPositiveAck: 'Thanks!',
89
103
  feedbackNegativeAck: "Noted — we'll improve",
90
104
  retry: 'Retry',
105
+ chartView: 'Chart',
106
+ chartDataView: 'Data',
107
+ chartViewSelector: 'Chart or data view',
108
+ chartDataTable: 'Chart data',
109
+ chartDataSummary: 'Exact values are available in the data view.',
110
+ chartNoData: 'No chart data',
91
111
  }
92
112
 
93
113
  export const MCPUIStringsContext = createContext<MCPUIStrings>(DEFAULT_MCPUI_STRINGS)
@@ -97,8 +117,8 @@ export const MCPUIStringsContext = createContext<MCPUIStrings>(DEFAULT_MCPUI_STR
97
117
  * `<MCPUIStringsProvider>` is mounted above — every renderer works
98
118
  * standalone with English chrome.
99
119
  */
100
- export function useMCPUIStrings(): MCPUIStrings {
101
- return useContext(MCPUIStringsContext)
120
+ export function useMCPUIStrings(): Required<MCPUIStrings> {
121
+ return mergeProps(DEFAULT_MCPUI_STRINGS, useContext(MCPUIStringsContext))
102
122
  }
103
123
 
104
124
  export interface MCPUIStringsProviderProps {
@@ -116,12 +136,11 @@ export interface MCPUIStringsProviderProps {
116
136
  * Merges the partial `strings` override over the English defaults.
117
137
  */
118
138
  export function MCPUIStringsProvider(props: MCPUIStringsProviderProps): JSX.Element {
119
- // `props.strings` is read inside the getter so a reactive override
120
- // (signal-backed) re-propagates ; for the common static case it is read
121
- // once at mount.
122
- const value = (): MCPUIStrings => ({ ...DEFAULT_MCPUI_STRINGS, ...props.strings })
139
+ // The function source keeps replacement `strings` objects reactive while
140
+ // mergeProps fills any omitted key without taking a one-time snapshot.
141
+ const value = mergeProps(DEFAULT_MCPUI_STRINGS, () => props.strings)
123
142
  return (
124
- <MCPUIStringsContext.Provider value={value()}>
143
+ <MCPUIStringsContext.Provider value={value}>
125
144
  {props.children}
126
145
  </MCPUIStringsContext.Provider>
127
146
  )
@@ -0,0 +1,41 @@
1
+ import { createSignal, onCleanup, onMount } from 'solid-js'
2
+ import { useEditableLayoutPositions } from '../components/EditableLayoutContext'
3
+
4
+ /** Stack read-only layouts in narrow containers, including desktop chat panels. */
5
+ export function createResponsiveGrid(breakpoint = 640) {
6
+ const [stacked, setStacked] = createSignal(false)
7
+ const preserveEditablePositions = useEditableLayoutPositions()
8
+ let container: HTMLElement | undefined
9
+
10
+ const ref = (element: HTMLElement) => { container = element }
11
+
12
+ // Editor canvases preserve authored coordinates at every width. This is a
13
+ // context-only opt-out, so ordinary/read-only renderers retain responsiveness.
14
+ if (preserveEditablePositions) return { stacked: () => false, ref }
15
+
16
+ onMount(() => {
17
+ if (!container) return
18
+ const element = container
19
+ const updateWidth = (width: number) => {
20
+ // Hidden/unmeasured containers retain their last layout until measurable.
21
+ if (Number.isFinite(width) && width > 0) setStacked(width < breakpoint)
22
+ }
23
+ const measure = () => updateWidth(element.getBoundingClientRect().width)
24
+ measure()
25
+
26
+ if (typeof ResizeObserver !== 'undefined') {
27
+ const observer = new ResizeObserver((entries) => {
28
+ for (const entry of entries) {
29
+ if (entry.target === element) updateWidth(entry.contentRect.width)
30
+ }
31
+ })
32
+ observer.observe(element)
33
+ onCleanup(() => observer.disconnect())
34
+ } else if (typeof window !== 'undefined') {
35
+ window.addEventListener('resize', measure)
36
+ onCleanup(() => window.removeEventListener('resize', measure))
37
+ }
38
+ })
39
+
40
+ return { stacked, ref }
41
+ }
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
6
6
  import { createEventEmitter, createCommandHandler, createChatBus, clarificationToPromptConfig, elicitationToPromptConfig } from './chat-bus'
7
- import type { ChatEvents, ChatCommands, ClarificationEvent, ElicitationEvent } from '../types/chat-bus'
7
+ import type { ClarificationEvent, ElicitationEvent } from '../types/chat-bus'
8
8
 
9
9
  describe('createEventEmitter', () => {
10
10
  it('emits events to subscribed listeners', () => {
@@ -6,8 +6,6 @@
6
6
  */
7
7
 
8
8
  import type {
9
- ChatEvents,
10
- ChatCommands,
11
9
  ChatEventEmitter,
12
10
  ChatCommandHandler,
13
11
  ChatBus,
@@ -124,14 +124,6 @@ export function createChatPromptController(): ChatPromptController {
124
124
  }
125
125
  }
126
126
 
127
- function clearPending(): void {
128
- if (pending) {
129
- cleanupAbort(pending)
130
- pending = null
131
- }
132
- setActivePrompt(null)
133
- }
134
-
135
127
  function handle(
136
128
  config: ChatPromptConfig,
137
129
  signal?: AbortSignal
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Tests for ComponentRegistry — all 19 component types registered
2
+ * Tests for ComponentRegistry — all 20 component types registered
3
3
  */
4
4
 
5
5
  import { describe, it, expect } from 'vitest'
@@ -7,13 +7,28 @@ import {
7
7
  validateAgainstRegistry,
8
8
  getComponentEntry,
9
9
  ComponentRegistry,
10
+ QuickchartRegistry,
11
+ TableRegistry,
10
12
  MapRegistry,
13
+ GraphRegistry,
11
14
  } from './component-registry'
12
15
  import type { ComponentType } from '../types'
13
- import { ComponentTypeSchema, MapComponentParamsSchema } from '@seed-ship/mcp-ui-spec'
16
+ import {
17
+ ChartComponentParamsSchema,
18
+ ChartTimeAxisSchema,
19
+ ChartTypeSchema,
20
+ ComponentTypeSchema,
21
+ GraphComponentParamsSchema,
22
+ GraphEdgeSchema,
23
+ GraphLayoutNameSchema,
24
+ GraphNodeSchema,
25
+ MapComponentParamsSchema,
26
+ TableComponentParamsSchema,
27
+ } from '@seed-ship/mcp-ui-spec'
14
28
 
15
29
  interface RegistrySchemaNode {
16
30
  type?: string
31
+ description?: string
17
32
  enum?: string[]
18
33
  oneOf?: RegistrySchemaNode[]
19
34
  anyOf?: RegistrySchemaNode[]
@@ -24,6 +39,7 @@ interface RegistrySchemaNode {
24
39
  minItems?: number
25
40
  maxItems?: number
26
41
  minLength?: number
42
+ minimum?: number
27
43
  }
28
44
 
29
45
  /** All 20 component types in the registry */
@@ -143,6 +159,69 @@ describe('MapRegistry ↔ map spec parity (UI-MAP-0a)', () => {
143
159
  })
144
160
  })
145
161
 
162
+ describe('visualization registry ↔ runtime spec parity', () => {
163
+ it('keeps the historical QuickchartRegistry export but describes the native renderer', () => {
164
+ expect(QuickchartRegistry.name).toBe('Chart.js')
165
+ expect(QuickchartRegistry.description).toContain('locally with Chart.js')
166
+ expect(QuickchartRegistry.description).toContain('host explicitly opts in')
167
+ })
168
+
169
+ it('advertises the complete chart params surface and all supported chart types', () => {
170
+ const properties = QuickchartRegistry.schema.properties as Record<string, RegistrySchemaNode>
171
+ expect(Object.keys(properties).sort()).toEqual(Object.keys(ChartComponentParamsSchema.shape).sort())
172
+ expect(properties.type.enum).toEqual(ChartTypeSchema.options)
173
+
174
+ const dataProperties = properties.data.properties!
175
+ const dataset = dataProperties.datasets.items as RegistrySchemaNode
176
+ const datasetProperties = dataset.properties!
177
+ expect(datasetProperties.data).not.toHaveProperty('oneOf')
178
+ expect(datasetProperties.data.anyOf).toHaveLength(2)
179
+ expect(ChartComponentParamsSchema.safeParse({
180
+ type: 'bar', data: { labels: [], datasets: [{ label: 'Empty series', data: [] }] },
181
+ }).success).toBe(true)
182
+ const pointBranch = datasetProperties.data.anyOf?.[1].items as RegistrySchemaNode
183
+ expect(pointBranch.required).toEqual(['x', 'y'])
184
+ expect(Object.keys(pointBranch.properties!).sort()).toEqual(['r', 'x', 'y'])
185
+ expect(pointBranch.properties?.r.minimum).toBe(0)
186
+
187
+ expect(Object.keys(properties.timeAxis.properties!).sort()).toEqual(
188
+ Object.keys(ChartTimeAxisSchema.shape).sort()
189
+ )
190
+ expect(properties.timeAxis.properties?.unit.enum).toEqual([
191
+ 'day', 'week', 'month', 'quarter', 'year',
192
+ ])
193
+ expect(properties.timeAxis.description).toContain('date adapter')
194
+ })
195
+
196
+ it('advertises the complete table surface including search and client paging controls', () => {
197
+ const properties = TableRegistry.schema.properties as Record<string, RegistrySchemaNode>
198
+ expect(Object.keys(properties).sort()).toEqual(Object.keys(TableComponentParamsSchema.shape).sort())
199
+ expect(properties.searchable.type).toBe('boolean')
200
+ expect(properties.searchPlaceholder.type).toBe('string')
201
+ expect(properties.pageSize).toMatchObject({ type: 'integer', minimum: 0 })
202
+ expect(properties.chatPageSize).toMatchObject({ type: 'integer', minimum: 0 })
203
+ expect(properties.initialPage).toMatchObject({ type: 'integer', minimum: 0 })
204
+ })
205
+
206
+ it('advertises only the seven canonical graph layouts, including object options', () => {
207
+ const properties = GraphRegistry.schema.properties as Record<string, RegistrySchemaNode>
208
+ expect(Object.keys(properties).sort()).toEqual(Object.keys(GraphComponentParamsSchema.shape).sort())
209
+ expect(properties).not.toHaveProperty('directed')
210
+
211
+ const [shorthand, objectForm] = properties.layout.oneOf!
212
+ expect(shorthand.enum).toEqual(GraphLayoutNameSchema.options)
213
+ expect(objectForm.properties?.type.enum).toEqual(GraphLayoutNameSchema.options)
214
+ expect(objectForm.properties).toHaveProperty('options')
215
+
216
+ const node = properties.nodes.items as RegistrySchemaNode
217
+ const edge = properties.edges.items as RegistrySchemaNode
218
+ expect(Object.keys(node.properties!).sort()).toEqual(Object.keys(GraphNodeSchema.shape).sort())
219
+ expect(Object.keys(edge.properties!).sort()).toEqual(Object.keys(GraphEdgeSchema.shape).sort())
220
+ expect(GraphRegistry.description).not.toContain('Degrades to an edge table')
221
+ expect(GraphRegistry.examples[0].component.params).not.toHaveProperty('directed')
222
+ })
223
+ })
224
+
146
225
  describe('ComponentRegistry', () => {
147
226
  describe('registry completeness', () => {
148
227
  it('has exactly 20 registered types', () => {