@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
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Component Registry Service
3
- * Phase 0: Static registry with Quickchart and Table definitions
3
+ * Phase 0: Static registry with Chart and Table definitions
4
4
  * Phase 1: Dynamic registry populated from /api/mcp/tools/list
5
5
  *
6
6
  * Provides component schemas for LLM prompt engineering
@@ -10,20 +10,23 @@ import type { ComponentRegistryEntry, ComponentType } from '../types'
10
10
  import { DEFAULT_RESOURCE_LIMITS } from './validation'
11
11
 
12
12
  /**
13
- * Quickchart Component Registry Entry
14
- * Based on Quickchart API documentation
13
+ * Chart Component Registry Entry
14
+ *
15
+ * The historical export name is retained for source compatibility. The
16
+ * renderer is local-first: Chart.js is the native implementation, while the
17
+ * external QuickChart fallback is a host-level opt-in.
15
18
  */
16
19
  export const QuickchartRegistry: ComponentRegistryEntry = {
17
20
  type: 'chart',
18
- name: 'Quickchart',
21
+ name: 'Chart.js',
19
22
  description:
20
- 'Render charts using Quickchart.io API. Supports bar, line, pie, doughnut, radar, and scatter charts. Best for visualizing numerical data with 2-10 data series and up to 1000 data points.',
23
+ 'Render charts locally with Chart.js. Supports bar, line, pie, doughnut, radar, scatter, bubble, and polarArea charts, including point and time-series data. An external QuickChart fallback is available only when the host explicitly opts in.',
21
24
  schema: {
22
25
  type: 'object',
23
26
  properties: {
24
27
  type: {
25
28
  type: 'string',
26
- enum: ['bar', 'line', 'pie', 'doughnut', 'radar', 'scatter'],
29
+ enum: ['bar', 'line', 'pie', 'doughnut', 'radar', 'scatter', 'bubble', 'polarArea'],
27
30
  description: 'Chart type',
28
31
  },
29
32
  title: {
@@ -45,8 +48,22 @@ export const QuickchartRegistry: ComponentRegistryEntry = {
45
48
  properties: {
46
49
  label: { type: 'string' },
47
50
  data: {
48
- type: 'array',
49
- items: { type: 'number' },
51
+ // Empty arrays match both branches; mirror Zod's inclusive union.
52
+ anyOf: [
53
+ { type: 'array', items: { type: 'number' } },
54
+ {
55
+ type: 'array',
56
+ items: {
57
+ type: 'object',
58
+ properties: {
59
+ x: { oneOf: [{ type: 'string' }, { type: 'number' }] },
60
+ y: { type: 'number' },
61
+ r: { type: 'number', minimum: 0 },
62
+ },
63
+ required: ['x', 'y'],
64
+ },
65
+ },
66
+ ],
50
67
  },
51
68
  backgroundColor: {
52
69
  oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }],
@@ -55,6 +72,8 @@ export const QuickchartRegistry: ComponentRegistryEntry = {
55
72
  oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }],
56
73
  },
57
74
  borderWidth: { type: 'number' },
75
+ fill: { oneOf: [{ type: 'boolean' }, { type: 'string' }] },
76
+ tension: { type: 'number' },
58
77
  },
59
78
  required: ['label', 'data'],
60
79
  },
@@ -64,8 +83,35 @@ export const QuickchartRegistry: ComponentRegistryEntry = {
64
83
  },
65
84
  options: {
66
85
  type: 'object',
67
- description: 'Chart.js options for customization',
86
+ description: 'Supported Chart.js options for customization',
87
+ properties: {
88
+ responsive: { type: 'boolean' },
89
+ maintainAspectRatio: { type: 'boolean' },
90
+ tension: { type: 'number' },
91
+ scales: {},
92
+ plugins: {},
93
+ },
94
+ },
95
+ renderer: {
96
+ type: 'string',
97
+ enum: ['native', 'iframe', 'auto'],
98
+ description: 'Renderer preference; external iframe use still requires host opt-in',
99
+ },
100
+ exportable: { type: 'boolean', description: 'Show the PNG export action' },
101
+ timeAxis: {
102
+ type: 'object',
103
+ description:
104
+ 'Time-series x-axis configuration; the host must install and register a compatible Chart.js date adapter',
105
+ properties: {
106
+ parser: { type: 'string' },
107
+ unit: { type: 'string', enum: ['day', 'week', 'month', 'quarter', 'year'] },
108
+ tooltipFormat: { type: 'string' },
109
+ min: { type: 'string' },
110
+ max: { type: 'string' },
111
+ },
68
112
  },
113
+ height: { type: 'string', description: 'Chart container height as a CSS value' },
114
+ className: { type: 'string' },
69
115
  },
70
116
  required: ['type', 'data'],
71
117
  },
@@ -128,7 +174,7 @@ export const TableRegistry: ComponentRegistryEntry = {
128
174
  type: 'table',
129
175
  name: 'DataTable',
130
176
  description:
131
- 'Render tabular data with sortable columns and pagination. Best for displaying structured records with up to 100 rows. Supports column width customization and cell formatting.',
177
+ 'Render tabular data with sortable columns, client-side search, context-aware paging, virtualization, and export controls. Best for structured records with up to 100 rows per payload.',
132
178
  schema: {
133
179
  type: 'object',
134
180
  properties: {
@@ -160,12 +206,68 @@ export const TableRegistry: ComponentRegistryEntry = {
160
206
  },
161
207
  pagination: {
162
208
  type: 'object',
209
+ description: 'Legacy server-pagination metadata',
163
210
  properties: {
164
211
  currentPage: { type: 'number' },
165
212
  pageSize: { type: 'number' },
166
213
  totalRows: { type: 'number' },
167
214
  },
168
215
  },
216
+ searchable: {
217
+ type: 'boolean',
218
+ description: 'Show the client-side search field (default: true)',
219
+ },
220
+ searchPlaceholder: { type: 'string', description: 'Search field placeholder' },
221
+ pageSize: {
222
+ type: 'integer',
223
+ minimum: 0,
224
+ description: 'Rows per page in expanded mode (default: 25; 0 disables client paging)',
225
+ },
226
+ chatPageSize: {
227
+ type: 'integer',
228
+ minimum: 0,
229
+ description: 'Rows per page in chat mode (default: min(10, pageSize); 0 disables client paging)',
230
+ },
231
+ initialPage: {
232
+ type: 'integer',
233
+ minimum: 0,
234
+ description: 'Initial zero-based client page',
235
+ },
236
+ virtualize: {
237
+ oneOf: [
238
+ { type: 'boolean' },
239
+ {
240
+ type: 'object',
241
+ properties: {
242
+ enabled: { type: 'boolean' },
243
+ rowHeight: { type: 'integer', minimum: 1 },
244
+ overscan: { type: 'integer', minimum: 0 },
245
+ threshold: { type: 'integer', minimum: 1 },
246
+ },
247
+ },
248
+ ],
249
+ },
250
+ exportable: {
251
+ oneOf: [
252
+ { type: 'boolean' },
253
+ {
254
+ type: 'object',
255
+ properties: {
256
+ formats: {
257
+ type: 'array',
258
+ items: { type: 'string', enum: ['csv', 'tsv', 'json'] },
259
+ },
260
+ filename: { type: 'string' },
261
+ },
262
+ },
263
+ ],
264
+ },
265
+ className: { type: 'string' },
266
+ citationMap: { type: 'object', description: 'Citation metadata keyed by marker id' },
267
+ maxHeight: {
268
+ oneOf: [{ type: 'string' }, { type: 'number' }],
269
+ description: "Inline table height cap; use 'auto' to disable it",
270
+ },
169
271
  },
170
272
  required: ['columns', 'rows'],
171
273
  },
@@ -868,19 +970,36 @@ export const GraphRegistry: ComponentRegistryEntry = {
868
970
  type: 'graph',
869
971
  name: 'NodeLinkGraph',
870
972
  description:
871
- 'Render a node-link graph (entities and their relationships) with @antv/g6. Best for provenance/source chains, dependency or process graphs, and ontology-lite entity/relation views. Degrades to an edge table when the graph engine is unavailable.',
973
+ 'Render a node-link graph (entities and their relationships) with the optional @antv/g6 peer. Best for provenance/source chains, dependency or process graphs, and ontology-lite entity/relation views.',
872
974
  schema: {
873
975
  type: 'object',
874
976
  properties: {
977
+ title: { type: 'string' },
875
978
  nodes: {
876
979
  type: 'array',
877
980
  description: 'Graph nodes (at least one required)',
981
+ minItems: 1,
878
982
  items: {
879
983
  type: 'object',
880
984
  properties: {
881
- id: { type: 'string' },
985
+ id: { type: 'string', minLength: 1 },
882
986
  label: { type: 'string' },
883
- group: { type: 'string' },
987
+ type: { type: 'string' },
988
+ size: {
989
+ oneOf: [
990
+ { type: 'number' },
991
+ {
992
+ type: 'array',
993
+ items: [{ type: 'number' }, { type: 'number' }],
994
+ additionalItems: false,
995
+ minItems: 2,
996
+ maxItems: 2,
997
+ },
998
+ ],
999
+ },
1000
+ weight: { type: 'number' },
1001
+ style: { type: 'object' },
1002
+ data: { type: 'object' },
884
1003
  },
885
1004
  required: ['id'],
886
1005
  },
@@ -891,20 +1010,46 @@ export const GraphRegistry: ComponentRegistryEntry = {
891
1010
  items: {
892
1011
  type: 'object',
893
1012
  properties: {
894
- source: { type: 'string' },
895
- target: { type: 'string' },
1013
+ source: { type: 'string', minLength: 1 },
1014
+ target: { type: 'string', minLength: 1 },
896
1015
  label: { type: 'string' },
1016
+ type: { type: 'string' },
897
1017
  weight: { type: 'number' },
1018
+ style: { type: 'object' },
1019
+ data: { type: 'object' },
898
1020
  },
899
1021
  required: ['source', 'target'],
900
1022
  },
901
1023
  },
902
1024
  layout: {
903
- type: 'string',
904
- enum: ['force', 'radial', 'grid', 'dagre', 'circular'],
905
- description: 'Layout algorithm (default: force)',
906
- },
907
- directed: { type: 'boolean', description: 'Render edges as directed (arrows)' },
1025
+ oneOf: [
1026
+ {
1027
+ type: 'string',
1028
+ enum: ['force', 'dagre', 'mindmap', 'tree', 'circular', 'grid', 'concentric'],
1029
+ },
1030
+ {
1031
+ type: 'object',
1032
+ properties: {
1033
+ type: {
1034
+ type: 'string',
1035
+ enum: ['force', 'dagre', 'mindmap', 'tree', 'circular', 'grid', 'concentric'],
1036
+ },
1037
+ options: { type: 'object' },
1038
+ },
1039
+ required: ['type'],
1040
+ },
1041
+ ],
1042
+ description: 'Layout shorthand or layout-plus-options object',
1043
+ },
1044
+ height: { type: 'string' },
1045
+ width: { type: 'string' },
1046
+ rendererPref: { type: 'string', enum: ['canvas'] },
1047
+ fitView: { type: 'boolean' },
1048
+ enableZoom: { type: 'boolean' },
1049
+ enableDrag: { type: 'boolean' },
1050
+ enableSelect: { type: 'boolean' },
1051
+ tooltip: { type: 'boolean' },
1052
+ className: { type: 'string' },
908
1053
  },
909
1054
  required: ['nodes'],
910
1055
  },
@@ -916,11 +1061,10 @@ export const GraphRegistry: ComponentRegistryEntry = {
916
1061
  type: 'graph',
917
1062
  position: { colStart: 1, colSpan: 12 },
918
1063
  params: {
919
- directed: true,
920
1064
  layout: 'dagre',
921
1065
  nodes: [
922
- { id: 'claim', label: 'Population = 522 250', group: 'claim' },
923
- { id: 'source', label: 'INSEE 2021', group: 'source' },
1066
+ { id: 'claim', label: 'Population = 522 250', weight: 2 },
1067
+ { id: 'source', label: 'INSEE 2021', weight: 1 },
924
1068
  ],
925
1069
  edges: [{ source: 'claim', target: 'source', label: 'derived from' }],
926
1070
  },
@@ -110,7 +110,7 @@ export function createTelemetryDispatcher(
110
110
  const result = sink(batch)
111
111
  // Promise rejections are silenced too (fail-open, §M.6.1).
112
112
  if (result && typeof (result as Promise<void>).then === 'function') {
113
- ;(result as Promise<void>).catch(() => {
113
+ (result as Promise<void>).catch(() => {
114
114
  /* silent */
115
115
  })
116
116
  }
@@ -5,7 +5,7 @@
5
5
  * without UNKNOWN_COMPONENT_TYPE errors.
6
6
  */
7
7
 
8
- import { describe, it, expect, vi } from 'vitest';
8
+ import { describe, it, expect } from 'vitest';
9
9
  import {
10
10
  validateComponent,
11
11
  validateChartComponent,
@@ -27,19 +27,6 @@ function makeComponent(type: ComponentType, params: Record<string, any> = {}): U
27
27
  };
28
28
  }
29
29
 
30
- /** Types that have explicit validation cases in validateComponent */
31
- const VALIDATED_TYPES: ComponentType[] = [
32
- 'chart',
33
- 'table',
34
- 'metric',
35
- 'text',
36
- 'iframe',
37
- 'image',
38
- 'link',
39
- 'action',
40
- 'artifact',
41
- ];
42
-
43
30
  /** Types that hit the default case (no specific validation) */
44
31
  const PASSTHROUGH_TYPES: ComponentType[] = [
45
32
  'code',
@@ -417,9 +417,12 @@ export function validateChartComponent(
417
417
  }
418
418
  // Detect point-based charts (scatter/bubble) or object data (time-series line)
419
419
  const chartType = params.type || 'bar';
420
- const firstDataPoint = params.data.datasets[0]?.data?.[0];
421
- const hasObjectData =
422
- typeof firstDataPoint === 'object' && firstDataPoint !== null && 'x' in firstDataPoint;
420
+ const usesPoints = (data: unknown[]) => data.some(value =>
421
+ typeof value === 'object' && value !== null && 'x' in value
422
+ );
423
+ const hasObjectData = params.data.datasets.some(dataset =>
424
+ Array.isArray(dataset.data) && usesPoints(dataset.data)
425
+ );
423
426
  const isPointChart = chartType === 'scatter' || chartType === 'bubble' || hasObjectData;
424
427
 
425
428
  // Labels required only for categorical charts (not scatter/bubble/time-series)
@@ -453,11 +456,12 @@ export function validateChartComponent(
453
456
  }
454
457
 
455
458
  // Length mismatch check — only for categorical charts, skip empty datasets
456
- if (!isPointChart && Array.isArray(params.data.labels)) {
459
+ if (chartType !== 'scatter' && chartType !== 'bubble' && Array.isArray(params.data.labels)) {
457
460
  const expectedLength = params.data.labels.length;
458
461
  for (const [index, dataset] of params.data.datasets.entries()) {
459
462
  if (
460
463
  Array.isArray(dataset.data) &&
464
+ !usesPoints(dataset.data) &&
461
465
  dataset.data.length > 0 &&
462
466
  dataset.data.length !== expectedLength
463
467
  ) {
@@ -473,14 +477,16 @@ export function validateChartComponent(
473
477
  // Data type validation — numbers for categorical, {x,y} objects for point charts
474
478
  for (const [index, dataset] of params.data.datasets.entries()) {
475
479
  if (!Array.isArray(dataset.data)) continue;
480
+ const datasetIsPointChart = chartType === 'scatter' || chartType === 'bubble' || usesPoints(dataset.data);
476
481
  for (const [dataIndex, value] of dataset.data.entries()) {
477
- if (isPointChart) {
482
+ if (datasetIsPointChart) {
478
483
  const vObj = value as any;
479
484
  if (
480
485
  typeof value !== 'object' ||
481
486
  value === null ||
482
487
  vObj.x == null ||
483
- typeof vObj.y !== 'number'
488
+ typeof vObj.y !== 'number' ||
489
+ (vObj.r !== undefined && (typeof vObj.r !== 'number' || !Number.isFinite(vObj.r) || vObj.r < 0))
484
490
  ) {
485
491
  errors.push({
486
492
  path: `params.data.datasets[${index}].data[${dataIndex}]`,
@@ -515,6 +521,17 @@ export function validateTableComponent(
515
521
  ): ValidationResult {
516
522
  const errors: ValidationResult['errors'] = [];
517
523
 
524
+ for (const key of ['pageSize', 'chatPageSize', 'initialPage'] as const) {
525
+ const value = params[key];
526
+ if (value !== undefined && (typeof value !== 'number' || !Number.isInteger(value) || value < 0)) {
527
+ errors.push({
528
+ path: `params.${key}`,
529
+ message: `${key} must be a nonnegative integer`,
530
+ code: 'INVALID_PAGINATION',
531
+ });
532
+ }
533
+ }
534
+
518
535
  // Validate row count
519
536
  if (params.rows.length > limits.maxTableRows) {
520
537
  errors.push({
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { validateComponent } from './validation'
3
+ import type { UIComponent } from '../types'
4
+
5
+ describe('visualization rendering boundary', () => {
6
+ it.each([false, true])('validates each dataset independently (points first: %s)', pointsFirst => {
7
+ const numeric = { label: 'Numeric', data: [1, 2] }
8
+ const points = { label: 'Points', data: [{ x: 1, y: 2, r: 3 }] }
9
+ const component: UIComponent = {
10
+ id: 'mixed-chart', type: 'chart', position: { colStart: 1, colSpan: 12 },
11
+ params: { type: 'line', data: { labels: ['A', 'B'], datasets: pointsFirst ? [points, numeric] : [numeric, points] } },
12
+ }
13
+ expect(validateComponent(component).valid).toBe(true)
14
+ points.data[0].r = -1
15
+ expect(validateComponent(component).valid).toBe(false)
16
+ })
17
+ for (const key of ['pageSize', 'chatPageSize', 'initialPage']) {
18
+ it.each([-1, 1.5, NaN, Infinity, '10'])(`rejects invalid ${key}: %s`, value => {
19
+ const component = {
20
+ id: 'table-bounds', type: 'table', position: { colStart: 1, colSpan: 12 },
21
+ params: { columns: [{ key: 'name', label: 'Name' }], rows: [], [key]: value },
22
+ } as UIComponent
23
+ const result = validateComponent(component)
24
+ expect(result.valid).toBe(false)
25
+ expect(result.errors).toContainEqual(expect.objectContaining({ path: `params.${key}` }))
26
+ })
27
+ }
28
+ it.each([-1, NaN, Infinity, '3', null])('rejects invalid bubble radius: %s', r => {
29
+ const component = {
30
+ id: 'bubble-bounds', type: 'chart', position: { colStart: 1, colSpan: 12 },
31
+ params: { type: 'bubble', data: { datasets: [{ label: 'Bubbles', data: [{ x: 1, y: 2, r }] }] } },
32
+ } as unknown as UIComponent
33
+ expect(validateComponent(component).valid).toBe(false)
34
+ })
35
+ it.each([undefined, 0, 4])('accepts optional/nonnegative bubble radius: %s', r => {
36
+ const component: UIComponent = {
37
+ id: 'bubble-valid', type: 'chart', position: { colStart: 1, colSpan: 12 },
38
+ params: { type: 'bubble', data: { datasets: [{ label: 'Bubbles', data: [{ x: 1, y: 2, r }] }] } },
39
+ }
40
+ expect(validateComponent(component).valid).toBe(true)
41
+ })
42
+ })
@@ -17,7 +17,7 @@ import type { ComponentType, LatLngPoint } from '@seed-ship/mcp-ui-spec'
17
17
  export type { ComponentType, LatLngPoint }
18
18
 
19
19
  /**
20
- * Chart types (powered by Quickchart)
20
+ * Chart types rendered natively with Chart.js.
21
21
  */
22
22
  export type ChartType = 'bar' | 'line' | 'pie' | 'doughnut' | 'radar' | 'scatter' | 'bubble' | 'polarArea'
23
23
 
@@ -81,7 +81,7 @@ export interface ChartComponentParams {
81
81
  labels: string[]
82
82
  datasets: Array<{
83
83
  label: string
84
- data: number[] | Array<{ x: string | number; y: number }>
84
+ data: number[] | Array<{ x: string | number; y: number; r?: number }>
85
85
  backgroundColor?: string | string[]
86
86
  borderColor?: string | string[]
87
87
  borderWidth?: number
@@ -101,8 +101,9 @@ export interface ChartComponentParams {
101
101
  /**
102
102
  * Force renderer type (Sprint 4)
103
103
  * - 'native': Use Chart.js directly (requires chart.js peer dependency)
104
- * - 'iframe': Use Quickchart.io iframe (default fallback)
105
- * - 'auto': Use native if Chart.js is available, otherwise iframe (default)
104
+ * - 'iframe': Request the external QuickChart renderer (requires host opt-in)
105
+ * - 'auto': Use native Chart.js when available; otherwise degrade locally
106
+ * unless the host explicitly enables the external fallback (default)
106
107
  */
107
108
  renderer?: 'native' | 'iframe' | 'auto'
108
109
  /**
@@ -112,6 +113,7 @@ export interface ChartComponentParams {
112
113
  /**
113
114
  * Time-series axis configuration (v3.1.0).
114
115
  * When set, x-axis labels are parsed as dates.
116
+ * The host must install and register a compatible Chart.js date adapter.
115
117
  */
116
118
  timeAxis?: {
117
119
  /** Date format for parsing labels (Chart.js adapter format, e.g. 'yyyy-MM-dd') */
@@ -190,6 +192,16 @@ export interface TableComponentParams {
190
192
  pageSize: number
191
193
  totalRows: number
192
194
  }
195
+ /** Show the accent-insensitive client-side search field (default: true). */
196
+ searchable?: boolean
197
+ /** Placeholder displayed in the client-side search field. */
198
+ searchPlaceholder?: string
199
+ /** Rows per page in expanded mode (default: 25). Set 0 to disable paging. */
200
+ pageSize?: number
201
+ /** Rows per page in chat mode (default: min(10, pageSize)). Set 0 to disable paging. */
202
+ chatPageSize?: number
203
+ /** Initial zero-based page for client-side paging. */
204
+ initialPage?: number
193
205
  /**
194
206
  * Enable table virtualization for large datasets (Sprint Ultimate U.3)
195
207
  * - true: Enable with default options
@@ -1019,7 +1031,11 @@ export interface GraphNode {
1019
1031
  /** Stable node id — referenced by edges. */
1020
1032
  id: string
1021
1033
  label?: string
1022
- /** Optional grouping/category key (drives default coloring). */
1034
+ type?: string
1035
+ size?: number | [number, number]
1036
+ style?: Record<string, unknown>
1037
+ data?: Record<string, unknown>
1038
+ /** @deprecated Compatibility field outside the canonical graph spec. */
1023
1039
  group?: string
1024
1040
  /** Generic ranking/importance signal (opaque to the lib). */
1025
1041
  weight?: number
@@ -1033,31 +1049,56 @@ export interface GraphEdge {
1033
1049
  /** Must match a `GraphNode.id`. */
1034
1050
  target: string
1035
1051
  label?: string
1052
+ type?: string
1036
1053
  weight?: number
1054
+ style?: Record<string, unknown>
1055
+ data?: Record<string, unknown>
1037
1056
  /** Passthrough G6 edge props (style, data, …). */
1038
1057
  [key: string]: unknown
1039
1058
  }
1040
1059
 
1041
- export type GraphLayout = 'force' | 'radial' | 'grid' | 'dagre' | 'circular' | 'concentric'
1060
+ /** Canonical layout names shared with `GraphLayoutNameSchema`. */
1061
+ export type GraphLayoutName =
1062
+ | 'force'
1063
+ | 'dagre'
1064
+ | 'mindmap'
1065
+ | 'tree'
1066
+ | 'circular'
1067
+ | 'grid'
1068
+ | 'concentric'
1069
+
1070
+ export type GraphLayout =
1071
+ | GraphLayoutName
1072
+ | { type: GraphLayoutName; options?: Record<string, unknown> }
1073
+ /** @deprecated `radial` predates the public spec; retained for source compatibility. */
1074
+ | 'radial'
1042
1075
 
1043
1076
  /**
1044
1077
  * Generic node-link graph component params. Domain-neutral by design — the
1045
1078
  * meaning of `weight`/`group` is decided by the consumer.
1046
1079
  */
1047
1080
  export interface GraphComponentParams {
1081
+ title?: string
1048
1082
  /** Nodes (at least one expected at runtime). */
1049
1083
  nodes: GraphNode[]
1050
1084
  edges?: GraphEdge[]
1051
1085
  layout?: GraphLayout
1052
- /** Render edges as directed (arrows). */
1086
+ /** @deprecated Accepted for source compatibility but not used by the renderer. */
1053
1087
  directed?: boolean
1054
1088
  /**
1055
1089
  * Rendering hint only — NOT wired; the renderer always uses the G6 canvas
1056
1090
  * default (cf. audit P0). Kept for forward-compat with the spec.
1057
1091
  */
1058
1092
  rendererPref?: 'canvas' | 'svg'
1059
- height?: number
1060
- width?: number
1093
+ /** CSS length in the canonical spec; numeric values remain for source compatibility. */
1094
+ height?: string | number
1095
+ /** CSS length in the canonical spec; numeric values remain for source compatibility. */
1096
+ width?: string | number
1097
+ fitView?: boolean
1098
+ enableZoom?: boolean
1099
+ enableDrag?: boolean
1100
+ enableSelect?: boolean
1101
+ tooltip?: boolean
1061
1102
  /** Custom CSS class. */
1062
1103
  className?: string
1063
1104
  /** Passthrough for forward-compat graph options. */
@@ -0,0 +1,66 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type {
3
+ ChartComponentParams,
4
+ ChartType,
5
+ GraphComponentParams,
6
+ GraphLayout,
7
+ GraphLayoutName,
8
+ TableComponentParams,
9
+ } from './index'
10
+
11
+ describe('public visualization contracts', () => {
12
+ it('types all eight chart variants, point data, and time-axis options', () => {
13
+ const chartTypes: ChartType[] = [
14
+ 'bar',
15
+ 'line',
16
+ 'pie',
17
+ 'doughnut',
18
+ 'radar',
19
+ 'scatter',
20
+ 'bubble',
21
+ 'polarArea',
22
+ ]
23
+ const params: ChartComponentParams = {
24
+ type: 'bubble',
25
+ data: {
26
+ labels: [],
27
+ datasets: [{ label: 'Points', data: [{ x: '2026-09-14', y: 3, r: 8 }] }],
28
+ },
29
+ timeAxis: { unit: 'day', parser: 'yyyy-MM-dd' },
30
+ }
31
+
32
+ expect(chartTypes).toHaveLength(8)
33
+ expect(params.timeAxis?.unit).toBe('day')
34
+ })
35
+
36
+ it('types the table search and context-aware paging controls', () => {
37
+ const params: TableComponentParams = {
38
+ columns: [{ key: 'name', label: 'Name', sortable: true }],
39
+ rows: [{ name: 'Ada' }],
40
+ searchable: true,
41
+ searchPlaceholder: 'Filter records',
42
+ pageSize: 30,
43
+ chatPageSize: 10,
44
+ initialPage: 0,
45
+ }
46
+
47
+ expect(params).toMatchObject({ searchable: true, pageSize: 30, chatPageSize: 10 })
48
+ })
49
+
50
+ it('types the seven canonical graph layouts and object-form options', () => {
51
+ const canonical: GraphLayoutName[] = [
52
+ 'force',
53
+ 'dagre',
54
+ 'mindmap',
55
+ 'tree',
56
+ 'circular',
57
+ 'grid',
58
+ 'concentric',
59
+ ]
60
+ const layout: GraphLayout = { type: 'dagre', options: { rankdir: 'TB' } }
61
+ const params: GraphComponentParams = { nodes: [{ id: 'root' }], layout }
62
+
63
+ expect(canonical).toHaveLength(7)
64
+ expect(params.layout).toEqual(layout)
65
+ })
66
+ })
@@ -47,13 +47,13 @@ describe('performance marks (v5.4.0 — B.4)', () => {
47
47
  })
48
48
 
49
49
  it('mark functions are no-ops when performance is undefined (SSR-safe)', () => {
50
- const originalPerf = (globalThis as any).performance
51
- ;(globalThis as any).performance = undefined
50
+ const originalPerf = (globalThis as any).performance;
51
+ (globalThis as any).performance = undefined
52
52
  try {
53
53
  expect(() => markRenderStart('ssr')).not.toThrow()
54
54
  expect(() => markRenderEnd('ssr')).not.toThrow()
55
55
  } finally {
56
- ;(globalThis as any).performance = originalPerf
56
+ (globalThis as any).performance = originalPerf
57
57
  }
58
58
  })
59
59
  })