@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.
- package/CHANGELOG.md +28 -0
- package/README.md +50 -3
- package/dist/adapters/index.d.ts +2 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/presentation.cjs +141 -0
- package/dist/adapters/presentation.cjs.map +1 -0
- package/dist/adapters/presentation.d.ts +68 -0
- package/dist/adapters/presentation.d.ts.map +1 -0
- package/dist/adapters/presentation.js +141 -0
- package/dist/adapters/presentation.js.map +1 -0
- package/dist/adapters.cjs +4 -0
- package/dist/adapters.cjs.map +1 -1
- package/dist/adapters.d.cts +2 -0
- package/dist/adapters.d.ts +2 -0
- package/dist/adapters.js +4 -0
- package/dist/adapters.js.map +1 -1
- package/dist/components/ChartJSRenderer.cjs +165 -65
- package/dist/components/ChartJSRenderer.cjs.map +1 -1
- package/dist/components/ChartJSRenderer.d.ts.map +1 -1
- package/dist/components/ChartJSRenderer.js +168 -68
- package/dist/components/ChartJSRenderer.js.map +1 -1
- package/dist/components/ComponentToolbar.cjs.map +1 -1
- package/dist/components/ComponentToolbar.d.ts.map +1 -1
- package/dist/components/ComponentToolbar.js.map +1 -1
- package/dist/components/EditableLayoutContext.cjs +17 -0
- package/dist/components/EditableLayoutContext.cjs.map +1 -0
- package/dist/components/EditableLayoutContext.d.ts +5 -0
- package/dist/components/EditableLayoutContext.d.ts.map +1 -0
- package/dist/components/EditableLayoutContext.js +17 -0
- package/dist/components/EditableLayoutContext.js.map +1 -0
- package/dist/components/EditableUIResourceRenderer.cjs +87 -84
- package/dist/components/EditableUIResourceRenderer.cjs.map +1 -1
- package/dist/components/EditableUIResourceRenderer.d.ts.map +1 -1
- package/dist/components/EditableUIResourceRenderer.js +87 -84
- package/dist/components/EditableUIResourceRenderer.js.map +1 -1
- package/dist/components/ElicitationForm.cjs.map +1 -1
- package/dist/components/ElicitationForm.js.map +1 -1
- package/dist/components/FeedbackInline.cjs +0 -1
- package/dist/components/FeedbackInline.cjs.map +1 -1
- package/dist/components/FeedbackInline.js +0 -1
- package/dist/components/FeedbackInline.js.map +1 -1
- package/dist/components/GraphRenderer.cjs.map +1 -1
- package/dist/components/GraphRenderer.d.ts.map +1 -1
- package/dist/components/GraphRenderer.js.map +1 -1
- package/dist/components/GridRenderer.cjs +12 -6
- package/dist/components/GridRenderer.cjs.map +1 -1
- package/dist/components/GridRenderer.d.ts.map +1 -1
- package/dist/components/GridRenderer.js +13 -7
- package/dist/components/GridRenderer.js.map +1 -1
- package/dist/components/MapRenderer.cjs.map +1 -1
- package/dist/components/MapRenderer.d.ts.map +1 -1
- package/dist/components/MapRenderer.js.map +1 -1
- package/dist/components/PresentationFeedback.cjs +0 -1
- package/dist/components/PresentationFeedback.cjs.map +1 -1
- package/dist/components/PresentationFeedback.js +0 -1
- package/dist/components/PresentationFeedback.js.map +1 -1
- package/dist/components/UIResourceRenderer.cjs +37 -22
- package/dist/components/UIResourceRenderer.cjs.map +1 -1
- package/dist/components/UIResourceRenderer.d.ts.map +1 -1
- package/dist/components/UIResourceRenderer.js +38 -23
- package/dist/components/UIResourceRenderer.js.map +1 -1
- package/dist/components/VerifiedText.cjs +0 -4
- package/dist/components/VerifiedText.cjs.map +1 -1
- package/dist/components/VerifiedText.d.ts.map +1 -1
- package/dist/components/VerifiedText.js +0 -4
- package/dist/components/VerifiedText.js.map +1 -1
- package/dist/components/chart-data-table.cjs +73 -0
- package/dist/components/chart-data-table.cjs.map +1 -0
- package/dist/components/chart-data-table.d.ts +23 -0
- package/dist/components/chart-data-table.d.ts.map +1 -0
- package/dist/components/chart-data-table.js +73 -0
- package/dist/components/chart-data-table.js.map +1 -0
- package/dist/context/MCPUIStringsContext.cjs +10 -9
- package/dist/context/MCPUIStringsContext.cjs.map +1 -1
- package/dist/context/MCPUIStringsContext.d.ts +17 -5
- package/dist/context/MCPUIStringsContext.d.ts.map +1 -1
- package/dist/context/MCPUIStringsContext.js +11 -10
- package/dist/context/MCPUIStringsContext.js.map +1 -1
- package/dist/hooks/createResponsiveGrid.cjs +37 -0
- package/dist/hooks/createResponsiveGrid.cjs.map +1 -0
- package/dist/hooks/createResponsiveGrid.d.ts +6 -0
- package/dist/hooks/createResponsiveGrid.d.ts.map +1 -0
- package/dist/hooks/createResponsiveGrid.js +37 -0
- package/dist/hooks/createResponsiveGrid.js.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-ui-spec/dist/schemas.cjs +13 -1
- package/dist/mcp-ui-spec/dist/schemas.cjs.map +1 -1
- package/dist/mcp-ui-spec/dist/schemas.js +13 -1
- package/dist/mcp-ui-spec/dist/schemas.js.map +1 -1
- package/dist/services/chat-bus.cjs.map +1 -1
- package/dist/services/chat-bus.d.ts.map +1 -1
- package/dist/services/chat-bus.js.map +1 -1
- package/dist/services/chat-prompt-controller.cjs.map +1 -1
- package/dist/services/chat-prompt-controller.d.ts.map +1 -1
- package/dist/services/chat-prompt-controller.js.map +1 -1
- package/dist/services/component-registry.cjs +162 -22
- package/dist/services/component-registry.cjs.map +1 -1
- package/dist/services/component-registry.d.ts +6 -3
- package/dist/services/component-registry.d.ts.map +1 -1
- package/dist/services/component-registry.js +162 -22
- package/dist/services/component-registry.js.map +1 -1
- package/dist/services/telemetry.cjs +0 -1
- package/dist/services/telemetry.cjs.map +1 -1
- package/dist/services/telemetry.js +0 -1
- package/dist/services/telemetry.js.map +1 -1
- package/dist/services/validation.cjs +21 -7
- package/dist/services/validation.cjs.map +1 -1
- package/dist/services/validation.d.ts.map +1 -1
- package/dist/services/validation.js +21 -7
- package/dist/services/validation.js.map +1 -1
- package/dist/types/index.d.ts +43 -8
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types.d.cts +43 -8
- package/dist/types.d.ts +43 -8
- package/docs/briefs/VISUALIZATION-FOUNDATION-2026-09-14.md +105 -0
- package/package.json +4 -4
- package/src/adapters/index.ts +18 -0
- package/src/adapters/presentation.test.ts +114 -0
- package/src/adapters/presentation.ts +229 -0
- package/src/components/ChartJSRenderer.test.tsx +325 -0
- package/src/components/ChartJSRenderer.tsx +162 -27
- package/src/components/ChatPrompt.test.tsx +1 -1
- package/src/components/ComponentToolbar.tsx +1 -1
- package/src/components/EditableLayoutContext.tsx +12 -0
- package/src/components/EditableUIResourceRenderer.tsx +11 -8
- package/src/components/ElicitationForm.tsx +1 -1
- package/src/components/FeedbackInline.tsx +1 -1
- package/src/components/GraphRenderer.test.tsx +1 -2
- package/src/components/GraphRenderer.tsx +0 -2
- package/src/components/GridRenderer.tsx +6 -4
- package/src/components/MapRenderer.tsx +0 -3
- package/src/components/PresentationFeedback.tsx +1 -1
- package/src/components/ResponsiveGrid.test.tsx +166 -0
- package/src/components/TableRenderer.pagination.test.tsx +31 -0
- package/src/components/UIResourceRenderer.tsx +14 -9
- package/src/components/VerifiedText.tsx +0 -5
- package/src/components/chart-data-table.ts +97 -0
- package/src/context/MCPUIStringsContext.test.tsx +60 -0
- package/src/context/MCPUIStringsContext.tsx +31 -12
- package/src/hooks/createResponsiveGrid.ts +41 -0
- package/src/services/chat-bus.test.ts +1 -1
- package/src/services/chat-bus.ts +0 -2
- package/src/services/chat-prompt-controller.ts +0 -8
- package/src/services/component-registry.test.ts +81 -2
- package/src/services/component-registry.ts +167 -23
- package/src/services/telemetry.ts +1 -1
- package/src/services/validation.test.ts +1 -14
- package/src/services/validation.ts +23 -6
- package/src/services/visualization-validation.test.ts +42 -0
- package/src/types/index.ts +50 -9
- package/src/types/visualization-contracts.test.ts +66 -0
- package/src/utils/perf.test.ts +3 -3
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -8,12 +8,23 @@
|
|
|
8
8
|
* ```
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
Component,
|
|
13
|
+
For,
|
|
14
|
+
createEffect,
|
|
15
|
+
createMemo,
|
|
16
|
+
createUniqueId,
|
|
17
|
+
onCleanup,
|
|
18
|
+
createSignal,
|
|
19
|
+
Show,
|
|
20
|
+
} from 'solid-js';
|
|
21
|
+
import type { Accessor, JSX } from 'solid-js';
|
|
12
22
|
import type { UIComponent, ChartComponentParams } from '../types';
|
|
13
23
|
import { ExpandableWrapper, useExpanded } from './ExpandableWrapper';
|
|
14
24
|
import { DegradedFallback } from './DegradedFallback';
|
|
15
|
-
import {
|
|
25
|
+
import { chartToDataTable } from './chart-data-table';
|
|
16
26
|
import { useTelemetry } from '../context/MCPUITelemetryContext';
|
|
27
|
+
import { useMCPUIStrings } from '../context/MCPUIStringsContext';
|
|
17
28
|
|
|
18
29
|
// Lazy load Chart.js to avoid bundling if not used
|
|
19
30
|
let ChartJS: any = null;
|
|
@@ -67,6 +78,10 @@ export interface ChartJSRendererProps {
|
|
|
67
78
|
toolbarVariant?: 'hover' | 'always-visible';
|
|
68
79
|
}
|
|
69
80
|
|
|
81
|
+
const ContextAwareChartLayout: Component<{
|
|
82
|
+
render: (isExpanded: Accessor<boolean>) => JSX.Element;
|
|
83
|
+
}> = (props) => props.render(useExpanded());
|
|
84
|
+
|
|
70
85
|
/**
|
|
71
86
|
* Native Chart.js renderer component
|
|
72
87
|
*
|
|
@@ -92,12 +107,17 @@ export interface ChartJSRendererProps {
|
|
|
92
107
|
export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
93
108
|
const [isLoading, setIsLoading] = createSignal(true);
|
|
94
109
|
const [error, setError] = createSignal<string>();
|
|
110
|
+
const [activeView, setActiveView] = createSignal<'chart' | 'data'>('chart');
|
|
95
111
|
let canvasRef: HTMLCanvasElement | undefined;
|
|
96
112
|
let chartInstance: any;
|
|
113
|
+
let renderVersion = 0;
|
|
97
114
|
|
|
98
115
|
const params = () => props.component.params as ChartComponentParams;
|
|
99
|
-
const
|
|
116
|
+
const tableData = createMemo(() => chartToDataTable(params()));
|
|
100
117
|
const telemetry = useTelemetry();
|
|
118
|
+
const strings = useMCPUIStrings();
|
|
119
|
+
const descriptionId = createUniqueId();
|
|
120
|
+
const title = () => params().title || strings.chartView;
|
|
101
121
|
|
|
102
122
|
// v6.1.0 — export visibility :
|
|
103
123
|
// - undefined / true → button shown (new default, was opt-in)
|
|
@@ -124,6 +144,7 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
124
144
|
|
|
125
145
|
// Access params to track dependencies
|
|
126
146
|
const chartParams = params();
|
|
147
|
+
const version = ++renderVersion;
|
|
127
148
|
|
|
128
149
|
setIsLoading(true);
|
|
129
150
|
setError(undefined);
|
|
@@ -131,6 +152,9 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
131
152
|
try {
|
|
132
153
|
const Chart = await loadChartJS();
|
|
133
154
|
|
|
155
|
+
// A newer prop update or an unmount superseded this async render.
|
|
156
|
+
if (version !== renderVersion || !canvasRef) return;
|
|
157
|
+
|
|
134
158
|
// Destroy previous instance
|
|
135
159
|
if (chartInstance) {
|
|
136
160
|
chartInstance.destroy();
|
|
@@ -180,6 +204,7 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
180
204
|
|
|
181
205
|
setIsLoading(false);
|
|
182
206
|
} catch (err) {
|
|
207
|
+
if (version !== renderVersion) return;
|
|
183
208
|
const error = err instanceof Error ? err : new Error('Chart rendering failed');
|
|
184
209
|
setError(error.message);
|
|
185
210
|
setIsLoading(false);
|
|
@@ -196,8 +221,16 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
196
221
|
}
|
|
197
222
|
});
|
|
198
223
|
|
|
224
|
+
// Chart.js may measure a zero-sized canvas while the data view is visible.
|
|
225
|
+
// Resize once Solid has revealed it again.
|
|
226
|
+
createEffect(() => {
|
|
227
|
+
if (activeView() !== 'chart' || !chartInstance) return;
|
|
228
|
+
queueMicrotask(() => chartInstance?.resize?.());
|
|
229
|
+
});
|
|
230
|
+
|
|
199
231
|
// Cleanup on unmount
|
|
200
232
|
onCleanup(() => {
|
|
233
|
+
renderVersion += 1;
|
|
201
234
|
if (chartInstance) {
|
|
202
235
|
chartInstance.destroy();
|
|
203
236
|
chartInstance = null;
|
|
@@ -206,25 +239,61 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
206
239
|
|
|
207
240
|
return (
|
|
208
241
|
<ExpandableWrapper
|
|
209
|
-
title={
|
|
242
|
+
title={title()}
|
|
210
243
|
copyData={copyDataJSON()}
|
|
211
244
|
copyLabel="Copy chart data (JSON)"
|
|
212
245
|
toolbarVariant={props.toolbarVariant}
|
|
213
246
|
>
|
|
214
|
-
<
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
247
|
+
<ContextAwareChartLayout
|
|
248
|
+
render={(isExpanded) => (
|
|
249
|
+
<div
|
|
250
|
+
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 ${
|
|
251
|
+
isExpanded() ? 'flex-1 min-h-0 flex flex-col' : ''
|
|
252
|
+
} ${params().className || ''}`}
|
|
253
|
+
>
|
|
254
|
+
<div class="flex items-center justify-between gap-3 mb-3 flex-shrink-0">
|
|
255
|
+
<div class="min-w-0">
|
|
221
256
|
<Show when={params().title}>
|
|
222
257
|
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">{params().title}</h3>
|
|
223
258
|
</Show>
|
|
259
|
+
</div>
|
|
260
|
+
<div class="flex items-center gap-2">
|
|
261
|
+
<div
|
|
262
|
+
class="inline-flex rounded-md border border-gray-200 p-0.5 dark:border-gray-600"
|
|
263
|
+
role="group"
|
|
264
|
+
aria-label={strings.chartViewSelector}
|
|
265
|
+
>
|
|
266
|
+
<button
|
|
267
|
+
type="button"
|
|
268
|
+
aria-pressed={activeView() === 'chart'}
|
|
269
|
+
class={`rounded px-2 py-1 text-xs font-medium transition-colors ${
|
|
270
|
+
activeView() === 'chart'
|
|
271
|
+
? 'bg-gray-900 text-white dark:bg-gray-100 dark:text-gray-900'
|
|
272
|
+
: 'text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700'
|
|
273
|
+
}`}
|
|
274
|
+
onClick={() => setActiveView('chart')}
|
|
275
|
+
>
|
|
276
|
+
{strings.chartView}
|
|
277
|
+
</button>
|
|
278
|
+
<button
|
|
279
|
+
type="button"
|
|
280
|
+
aria-pressed={activeView() === 'data'}
|
|
281
|
+
class={`rounded px-2 py-1 text-xs font-medium transition-colors ${
|
|
282
|
+
activeView() === 'data'
|
|
283
|
+
? 'bg-gray-900 text-white dark:bg-gray-100 dark:text-gray-900'
|
|
284
|
+
: 'text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700'
|
|
285
|
+
}`}
|
|
286
|
+
onClick={() => setActiveView('data')}
|
|
287
|
+
>
|
|
288
|
+
{strings.chartDataView}
|
|
289
|
+
</button>
|
|
290
|
+
</div>
|
|
224
291
|
<Show when={exportEnabled()}>
|
|
225
292
|
<button
|
|
293
|
+
type="button"
|
|
226
294
|
onClick={handleExportPNG}
|
|
227
|
-
|
|
295
|
+
disabled={activeView() !== 'chart' || isLoading() || Boolean(error())}
|
|
296
|
+
class="opacity-0 group-hover:opacity-60 hover:!opacity-100 disabled:!opacity-30 disabled:cursor-not-allowed px-2 py-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-all shadow-sm"
|
|
228
297
|
title="Download PNG"
|
|
229
298
|
aria-label="Download chart as PNG"
|
|
230
299
|
>
|
|
@@ -244,40 +313,106 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
244
313
|
</button>
|
|
245
314
|
</Show>
|
|
246
315
|
</div>
|
|
247
|
-
</
|
|
316
|
+
</div>
|
|
248
317
|
|
|
249
|
-
<
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
|
|
253
|
-
<span class="text-sm text-gray-500 dark:text-gray-400">Loading chart...</span>
|
|
254
|
-
</div>
|
|
255
|
-
</div>
|
|
256
|
-
</Show>
|
|
318
|
+
<p id={descriptionId} class="sr-only">
|
|
319
|
+
{title()}. {strings.chartDataSummary}
|
|
320
|
+
</p>
|
|
257
321
|
|
|
258
322
|
{/* Fallback ladder (P2.5): degrade to a series table on render error
|
|
259
323
|
instead of a bare "Chart Error" message. */}
|
|
260
|
-
<Show when={error()}>
|
|
324
|
+
<Show when={error() && activeView() === 'chart'}>
|
|
261
325
|
<DegradedFallback
|
|
262
326
|
message={`Chart rendering failed: ${error()}`}
|
|
263
327
|
caption="Showing the chart data as a table — the interactive chart is unavailable."
|
|
264
|
-
{...
|
|
328
|
+
{...tableData()}
|
|
265
329
|
/>
|
|
266
330
|
</Show>
|
|
267
331
|
|
|
332
|
+
<Show when={activeView() === 'data'}>
|
|
333
|
+
<div
|
|
334
|
+
class={`w-full overflow-auto rounded border border-gray-200 dark:border-gray-700 ${
|
|
335
|
+
isExpanded() ? 'flex-1 min-h-0' : ''
|
|
336
|
+
}`}
|
|
337
|
+
style={
|
|
338
|
+
isExpanded()
|
|
339
|
+
? undefined
|
|
340
|
+
: { height: params().height || '250px', 'max-height': '70vh' }
|
|
341
|
+
}
|
|
342
|
+
>
|
|
343
|
+
<table class="w-full border-collapse text-left text-sm">
|
|
344
|
+
<caption class="sr-only">
|
|
345
|
+
{title()} — {strings.chartDataTable}
|
|
346
|
+
</caption>
|
|
347
|
+
<thead class="bg-gray-50 dark:bg-gray-900/40">
|
|
348
|
+
<tr>
|
|
349
|
+
<For each={tableData().columns}>
|
|
350
|
+
{(column) => (
|
|
351
|
+
<th scope="col" class="px-3 py-2 font-medium text-gray-900 dark:text-gray-100">
|
|
352
|
+
{column}
|
|
353
|
+
</th>
|
|
354
|
+
)}
|
|
355
|
+
</For>
|
|
356
|
+
</tr>
|
|
357
|
+
</thead>
|
|
358
|
+
<tbody>
|
|
359
|
+
<For each={tableData().rows}>
|
|
360
|
+
{(row) => (
|
|
361
|
+
<tr class="border-t border-gray-100 dark:border-gray-700">
|
|
362
|
+
<For each={tableData().columns}>
|
|
363
|
+
{(_column, index) => (
|
|
364
|
+
<td class="px-3 py-2 text-gray-700 dark:text-gray-300">
|
|
365
|
+
{String(row[index()] ?? '')}
|
|
366
|
+
</td>
|
|
367
|
+
)}
|
|
368
|
+
</For>
|
|
369
|
+
</tr>
|
|
370
|
+
)}
|
|
371
|
+
</For>
|
|
372
|
+
<Show when={tableData().rows.length === 0}>
|
|
373
|
+
<tr>
|
|
374
|
+
<td
|
|
375
|
+
colSpan={Math.max(1, tableData().columns.length)}
|
|
376
|
+
class="px-3 py-6 text-center text-gray-500 dark:text-gray-400"
|
|
377
|
+
>
|
|
378
|
+
{strings.chartNoData}
|
|
379
|
+
</td>
|
|
380
|
+
</tr>
|
|
381
|
+
</Show>
|
|
382
|
+
</tbody>
|
|
383
|
+
</table>
|
|
384
|
+
</div>
|
|
385
|
+
</Show>
|
|
386
|
+
|
|
268
387
|
<div
|
|
269
|
-
class={`w-full ${isExpanded() ? 'flex-1 min-h-0' : ''}`}
|
|
388
|
+
class={`relative w-full ${isExpanded() ? 'flex-1 min-h-0' : ''}`}
|
|
270
389
|
style={
|
|
271
|
-
error()
|
|
390
|
+
error() || activeView() === 'data'
|
|
272
391
|
? { display: 'none' }
|
|
273
392
|
: isExpanded()
|
|
274
393
|
? { height: '100%', display: 'block' }
|
|
275
394
|
: { height: params().height || '250px', display: 'block' }
|
|
276
395
|
}
|
|
277
396
|
>
|
|
278
|
-
<canvas
|
|
397
|
+
<canvas
|
|
398
|
+
ref={canvasRef}
|
|
399
|
+
role="img"
|
|
400
|
+
aria-label={title()}
|
|
401
|
+
aria-describedby={descriptionId}
|
|
402
|
+
aria-hidden={activeView() !== 'chart' || Boolean(error())}
|
|
403
|
+
/>
|
|
404
|
+
<Show when={isLoading() && activeView() === 'chart'}>
|
|
405
|
+
<div class="absolute inset-0 flex items-center justify-center bg-white/80 dark:bg-gray-800/80">
|
|
406
|
+
<div class="flex flex-col items-center gap-2">
|
|
407
|
+
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
|
|
408
|
+
<span class="text-sm text-gray-500 dark:text-gray-400">Loading chart...</span>
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
</Show>
|
|
279
412
|
</div>
|
|
280
|
-
|
|
413
|
+
</div>
|
|
414
|
+
)}
|
|
415
|
+
/>
|
|
281
416
|
</ExpandableWrapper>
|
|
282
417
|
);
|
|
283
418
|
};
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
7
7
|
import { render, fireEvent, cleanup } from '@solidjs/testing-library'
|
|
8
8
|
import { ChatPrompt } from './ChatPrompt'
|
|
9
|
-
import type { ChatPromptConfig
|
|
9
|
+
import type { ChatPromptConfig } from '../types/chat-bus'
|
|
10
10
|
|
|
11
11
|
describe('ChatPrompt', () => {
|
|
12
12
|
beforeEach(() => cleanup())
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* v2.2.5: Consistent icon set, position, and hover behavior across all components
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { Component, For,
|
|
6
|
+
import { Component, For, createSignal } from 'solid-js'
|
|
7
7
|
|
|
8
8
|
/** SVG icon paths for toolbar actions */
|
|
9
9
|
const ICONS = {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Private context: editor canvases must keep authored grid positions. */
|
|
2
|
+
import { createContext, useContext, type ParentComponent } from 'solid-js'
|
|
3
|
+
|
|
4
|
+
const EditableLayoutContext = createContext(false)
|
|
5
|
+
|
|
6
|
+
export const EditableLayoutProvider: ParentComponent = (props) => (
|
|
7
|
+
<EditableLayoutContext.Provider value={true}>{props.children}</EditableLayoutContext.Provider>
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
export function useEditableLayoutPositions(): boolean {
|
|
11
|
+
return useContext(EditableLayoutContext)
|
|
12
|
+
}
|
|
@@ -26,6 +26,7 @@ import { useDragDrop } from '../hooks/useDragDrop'
|
|
|
26
26
|
import { useResize, ResizeEdge } from '../hooks/useResize'
|
|
27
27
|
import { DraggableGridItem } from './DraggableGridItem'
|
|
28
28
|
import { UIResourceRenderer } from './UIResourceRenderer'
|
|
29
|
+
import { EditableLayoutProvider } from './EditableLayoutContext'
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Props for EditableUIResourceRenderer
|
|
@@ -204,13 +205,14 @@ export const EditableUIResourceRenderer: Component<EditableUIResourceRendererPro
|
|
|
204
205
|
}
|
|
205
206
|
|
|
206
207
|
return (
|
|
207
|
-
<
|
|
208
|
-
<div
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
208
|
+
<EditableLayoutProvider>
|
|
209
|
+
<div class={`w-full editable-ui-renderer ${props.class || ''}`}>
|
|
210
|
+
<div
|
|
211
|
+
ref={setGridContainerRef}
|
|
212
|
+
class="grid relative"
|
|
213
|
+
style={gridContainerStyle()}
|
|
214
|
+
data-grid-container
|
|
215
|
+
>
|
|
214
216
|
{/* Grid overlay */}
|
|
215
217
|
<GridOverlay
|
|
216
218
|
columns={props.layout.grid.columns}
|
|
@@ -260,8 +262,9 @@ export const EditableUIResourceRenderer: Component<EditableUIResourceRendererPro
|
|
|
260
262
|
)
|
|
261
263
|
}}
|
|
262
264
|
</For>
|
|
265
|
+
</div>
|
|
263
266
|
</div>
|
|
264
|
-
</
|
|
267
|
+
</EditableLayoutProvider>
|
|
265
268
|
)
|
|
266
269
|
}
|
|
267
270
|
|
|
@@ -78,7 +78,7 @@ export const ElicitationForm: Component<ElicitationFormProps> = (props) => {
|
|
|
78
78
|
|
|
79
79
|
const handleSubmit = (response: ChatPromptResponse): void => {
|
|
80
80
|
if (response.dismissed) {
|
|
81
|
-
|
|
81
|
+
(props.onDecline ?? props.onCancel)?.()
|
|
82
82
|
return
|
|
83
83
|
}
|
|
84
84
|
props.onAccept(extractContent(response, props.event))
|
|
@@ -84,7 +84,7 @@ export const FeedbackInline: Component<FeedbackInlineProps> = (props) => {
|
|
|
84
84
|
// swallow it — feedback is best-effort by design.
|
|
85
85
|
const result = props.onSubmit(value, props.context)
|
|
86
86
|
if (result && typeof (result as Promise<void>).catch === 'function') {
|
|
87
|
-
|
|
87
|
+
(result as Promise<void>).catch(() => {
|
|
88
88
|
/* non-blocking */
|
|
89
89
|
})
|
|
90
90
|
}
|
|
@@ -16,14 +16,13 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { describe, it, expect, beforeEach } from 'vitest'
|
|
19
|
-
import {
|
|
19
|
+
import { cleanup, waitFor } from '@solidjs/testing-library'
|
|
20
20
|
import {
|
|
21
21
|
graphToMermaid,
|
|
22
22
|
graphToJSON,
|
|
23
23
|
isG6Available,
|
|
24
24
|
GraphRenderer,
|
|
25
25
|
} from './GraphRenderer'
|
|
26
|
-
import { UIResourceRenderer } from './UIResourceRenderer'
|
|
27
26
|
import { validateComponent } from '../services/validation'
|
|
28
27
|
import type { GraphComponentParams } from '@seed-ship/mcp-ui-spec'
|
|
29
28
|
import type { UIComponent } from '../types'
|
|
@@ -23,8 +23,6 @@ import type { UIComponent } from '../types';
|
|
|
23
23
|
import type {
|
|
24
24
|
GraphComponentParams,
|
|
25
25
|
GraphLayout,
|
|
26
|
-
GraphNode,
|
|
27
|
-
GraphEdge,
|
|
28
26
|
} from '@seed-ship/mcp-ui-spec';
|
|
29
27
|
import { ExpandableWrapper, useExpanded } from './ExpandableWrapper';
|
|
30
28
|
import { PortalDropdownMenu } from './PortalDropdownMenu';
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { Component, For, createMemo } from 'solid-js'
|
|
8
8
|
import type { UIComponent, GridPosition } from '../types'
|
|
9
9
|
import { useRenderContext } from './RenderContext'
|
|
10
|
+
import { createResponsiveGrid } from '../hooks/createResponsiveGrid'
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Parameters for GridRenderer component
|
|
@@ -87,6 +88,7 @@ function buildGridTemplateAreas(areas: string[][]): string {
|
|
|
87
88
|
* Renders a CSS Grid container with nested UIComponents
|
|
88
89
|
*/
|
|
89
90
|
export const GridRenderer: Component<GridRendererProps> = (props) => {
|
|
91
|
+
const responsiveGrid = createResponsiveGrid()
|
|
90
92
|
// Use render context to avoid circular dependency
|
|
91
93
|
const { renderComponent } = useRenderContext()
|
|
92
94
|
|
|
@@ -105,13 +107,13 @@ export const GridRenderer: Component<GridRendererProps> = (props) => {
|
|
|
105
107
|
// Build grid container style
|
|
106
108
|
const gridContainerStyle = createMemo(() => {
|
|
107
109
|
const p = params()
|
|
108
|
-
let style = `display: grid; grid-template-columns: repeat(${p.columns}, 1fr); gap: ${p.gap}`
|
|
110
|
+
let style = `display: grid; grid-template-columns: repeat(${responsiveGrid.stacked() ? 1 : p.columns}, minmax(0, 1fr)); gap: ${p.gap}`
|
|
109
111
|
|
|
110
112
|
if (p.minRowHeight) {
|
|
111
113
|
style += `; grid-auto-rows: minmax(${p.minRowHeight}, auto)`
|
|
112
114
|
}
|
|
113
115
|
|
|
114
|
-
if (p.areas && p.areas.length > 0) {
|
|
116
|
+
if (!responsiveGrid.stacked() && p.areas && p.areas.length > 0) {
|
|
115
117
|
style += `; grid-template-areas: ${buildGridTemplateAreas(p.areas)}`
|
|
116
118
|
}
|
|
117
119
|
|
|
@@ -124,11 +126,11 @@ export const GridRenderer: Component<GridRendererProps> = (props) => {
|
|
|
124
126
|
data-component-type="grid"
|
|
125
127
|
data-component-id={props.component.id}
|
|
126
128
|
>
|
|
127
|
-
<div class="p-4 h-full" style={gridContainerStyle()}>
|
|
129
|
+
<div ref={responsiveGrid.ref} class="p-4 h-full" style={gridContainerStyle()} data-mcp-ui-grid data-mcp-ui-stacked={responsiveGrid.stacked()}>
|
|
128
130
|
<For each={params().children}>
|
|
129
131
|
{(child) => (
|
|
130
132
|
<div
|
|
131
|
-
style={getGridItemStyle(child.position, params().areas)}
|
|
133
|
+
style={responsiveGrid.stacked() ? 'grid-column: 1 / -1; grid-row: auto' : getGridItemStyle(child.position, params().areas)}
|
|
132
134
|
class="min-w-0 h-full"
|
|
133
135
|
>
|
|
134
136
|
{/* Use RenderContext for recursive rendering (avoids circular dependency) */}
|
|
@@ -12,8 +12,6 @@ import type {
|
|
|
12
12
|
MapClusterOptions,
|
|
13
13
|
MapGeoJSONStyle,
|
|
14
14
|
MapPopupConfig,
|
|
15
|
-
MapLayer,
|
|
16
|
-
MapPMTilesConfig,
|
|
17
15
|
} from '../types';
|
|
18
16
|
import { ExpandableWrapper, useExpanded } from './ExpandableWrapper';
|
|
19
17
|
import { DegradedFallback } from './DegradedFallback';
|
|
@@ -458,7 +456,6 @@ export const MapRenderer: Component<MapRendererProps> = (props) => {
|
|
|
458
456
|
setPmtilesError(null);
|
|
459
457
|
if (p?.pmtiles) {
|
|
460
458
|
try {
|
|
461
|
-
// @ts-ignore — optional peer dependency, may not be installed
|
|
462
459
|
const protomaps = await import(/* @vite-ignore */ 'protomaps-leaflet');
|
|
463
460
|
const pmConfig = p.pmtiles;
|
|
464
461
|
|
|
@@ -170,7 +170,7 @@ export const PresentationFeedback: Component<PresentationFeedbackProps> = (props
|
|
|
170
170
|
// break the UI (mirrors FeedbackInline).
|
|
171
171
|
const result = props.onSubmit(feedback)
|
|
172
172
|
if (result && typeof (result as Promise<void>).catch === 'function') {
|
|
173
|
-
|
|
173
|
+
(result as Promise<void>).catch(() => {
|
|
174
174
|
/* non-blocking */
|
|
175
175
|
})
|
|
176
176
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { cleanup, render } from '@solidjs/testing-library'
|
|
3
|
+
import { createSignal } from 'solid-js'
|
|
4
|
+
import { UIResourceRenderer } from './UIResourceRenderer'
|
|
5
|
+
import { EditableUIResourceRenderer } from './EditableUIResourceRenderer'
|
|
6
|
+
import { GridRenderer } from './GridRenderer'
|
|
7
|
+
import { RenderProvider } from './RenderContext'
|
|
8
|
+
import type { UIComponent, UILayout } from '../types'
|
|
9
|
+
|
|
10
|
+
const observers: Array<{
|
|
11
|
+
callback: ResizeObserverCallback
|
|
12
|
+
element?: Element
|
|
13
|
+
disconnect: ReturnType<typeof vi.fn>
|
|
14
|
+
}> = []
|
|
15
|
+
|
|
16
|
+
function resize(element: Element, width: number) {
|
|
17
|
+
const observer = observers.find((item) => item.element === element)
|
|
18
|
+
expect(observer).toBeDefined()
|
|
19
|
+
observer!.callback([{ target: element, contentRect: { width } } as ResizeObserverEntry], {} as ResizeObserver)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function textComponent(id: string, colStart = 1, colSpan = 6): UIComponent {
|
|
23
|
+
return { id, type: 'text', position: { colStart, colSpan, rowStart: 2, rowSpan: 2 }, params: { content: id } }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function layout(): UILayout {
|
|
27
|
+
return { id: 'responsive-test', grid: { columns: 12, gap: '1rem' }, components: [textComponent('first'), textComponent('second', 7)] }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
observers.length = 0
|
|
32
|
+
vi.stubGlobal('ResizeObserver', class {
|
|
33
|
+
item: typeof observers[number]
|
|
34
|
+
constructor(callback: ResizeObserverCallback) {
|
|
35
|
+
this.item = { callback, disconnect: vi.fn() }
|
|
36
|
+
observers.push(this.item)
|
|
37
|
+
}
|
|
38
|
+
observe(element: Element) { this.item.element = element }
|
|
39
|
+
disconnect() { this.item.disconnect() }
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
cleanup()
|
|
45
|
+
vi.restoreAllMocks()
|
|
46
|
+
vi.unstubAllGlobals()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('responsive read-only grids', () => {
|
|
50
|
+
it('stacks inside a narrow container and restores explicit desktop positions without mutating input', () => {
|
|
51
|
+
const input = layout()
|
|
52
|
+
const original = JSON.stringify(input)
|
|
53
|
+
const { container } = render(() => <UIResourceRenderer content={input} />)
|
|
54
|
+
const grid = container.querySelector<HTMLElement>('[data-mcp-ui-grid]')!
|
|
55
|
+
const first = grid.children[0] as HTMLElement
|
|
56
|
+
const second = grid.children[1] as HTMLElement
|
|
57
|
+
expect(grid.style.gridTemplateColumns).toBe('repeat(12, minmax(0, 1fr))')
|
|
58
|
+
resize(grid, 420)
|
|
59
|
+
expect(grid.dataset.mcpUiStacked).toBe('true')
|
|
60
|
+
expect(grid.style.gridTemplateColumns).toBe('repeat(1, minmax(0, 1fr))')
|
|
61
|
+
expect(first.style.gridColumn).toBe('1 / -1')
|
|
62
|
+
expect(second.style.gridRow).toBe('auto')
|
|
63
|
+
expect(grid.textContent!.indexOf('first')).toBeLessThan(grid.textContent!.indexOf('second'))
|
|
64
|
+
resize(grid, 640)
|
|
65
|
+
expect(grid.dataset.mcpUiStacked).toBe('false')
|
|
66
|
+
expect(second.style.gridColumn).toBe('7 / span 6')
|
|
67
|
+
expect(second.style.gridRow).toBe('2 / span 2')
|
|
68
|
+
expect(JSON.stringify(input)).toBe(original)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('uses the container width on initial mount, independently of the viewport', () => {
|
|
72
|
+
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue({ width: 360 } as DOMRect)
|
|
73
|
+
const { container } = render(() => <UIResourceRenderer content={layout()} />)
|
|
74
|
+
expect(window.innerWidth).toBeGreaterThan(640)
|
|
75
|
+
expect(container.querySelector<HTMLElement>('[data-mcp-ui-grid]')!.dataset.mcpUiStacked).toBe('true')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('does not switch on hidden or invalid observer measurements and disconnects on unmount', () => {
|
|
79
|
+
const { container, unmount } = render(() => <UIResourceRenderer content={layout()} />)
|
|
80
|
+
const grid = container.querySelector<HTMLElement>('[data-mcp-ui-grid]')!
|
|
81
|
+
resize(grid, 320)
|
|
82
|
+
for (const width of [0, -1, Number.NaN]) resize(grid, width)
|
|
83
|
+
expect(grid.dataset.mcpUiStacked).toBe('true')
|
|
84
|
+
const observer = observers.find((item) => item.element === grid)!
|
|
85
|
+
unmount()
|
|
86
|
+
expect(observer.disconnect).toHaveBeenCalledOnce()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('keeps components without a position inside the configured column count', () => {
|
|
90
|
+
const input = layout()
|
|
91
|
+
input.grid.columns = 2
|
|
92
|
+
input.components = [{ id: 'unpositioned', type: 'text', params: { content: 'Full width' } } as UIComponent]
|
|
93
|
+
const { container } = render(() => <UIResourceRenderer content={input} />)
|
|
94
|
+
const grid = container.querySelector<HTMLElement>('[data-mcp-ui-grid]')!
|
|
95
|
+
expect((grid.children[0] as HTMLElement).style.gridColumn).toBe('1 / -1')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('measures nested grids independently and removes named areas only while stacked', () => {
|
|
99
|
+
const input: UIComponent = {
|
|
100
|
+
id: 'nested', type: 'grid', position: { colStart: 1, colSpan: 12 },
|
|
101
|
+
params: { columns: 2, areas: [['left', 'right']], children: [textComponent('left', 1, 1), textComponent('right', 2, 1)] },
|
|
102
|
+
}
|
|
103
|
+
const { container } = render(() => (
|
|
104
|
+
<RenderProvider renderComponent={(component) => <span>{component.id}</span>}>
|
|
105
|
+
<GridRenderer component={input} />
|
|
106
|
+
</RenderProvider>
|
|
107
|
+
))
|
|
108
|
+
const grid = container.querySelector<HTMLElement>('[data-mcp-ui-grid]')!
|
|
109
|
+
expect(grid.style.gridTemplateAreas).toBe('"left right"')
|
|
110
|
+
resize(grid, 500)
|
|
111
|
+
expect(grid.style.gridTemplateAreas).toBe('')
|
|
112
|
+
expect((grid.children[1] as HTMLElement).style.gridColumn).toBe('1 / -1')
|
|
113
|
+
resize(grid, 800)
|
|
114
|
+
expect(grid.style.gridTemplateAreas).toBe('"left right"')
|
|
115
|
+
expect((grid.children[1] as HTMLElement).style.gridColumn).toBe('2 / span 1')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('updates layout components while retaining the narrow-container presentation', () => {
|
|
119
|
+
const [input, setInput] = createSignal(layout())
|
|
120
|
+
const { container } = render(() => <UIResourceRenderer content={input()} />)
|
|
121
|
+
const grid = container.querySelector<HTMLElement>('[data-mcp-ui-grid]')!
|
|
122
|
+
resize(grid, 400)
|
|
123
|
+
setInput({ ...layout(), components: [textComponent('replacement')] })
|
|
124
|
+
expect(grid.children.length).toBe(1)
|
|
125
|
+
expect(grid.textContent).toContain('replacement')
|
|
126
|
+
expect((grid.children[0] as HTMLElement).style.gridColumn).toBe('1 / -1')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('falls back to window resize when ResizeObserver is unavailable and cleans up', () => {
|
|
130
|
+
vi.stubGlobal('ResizeObserver', undefined)
|
|
131
|
+
let width = 900
|
|
132
|
+
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(() => ({ width }) as DOMRect)
|
|
133
|
+
const removeListener = vi.spyOn(window, 'removeEventListener')
|
|
134
|
+
const { container, unmount } = render(() => <UIResourceRenderer content={layout()} />)
|
|
135
|
+
const grid = container.querySelector<HTMLElement>('[data-mcp-ui-grid]')!
|
|
136
|
+
width = 400
|
|
137
|
+
window.dispatchEvent(new Event('resize'))
|
|
138
|
+
expect(grid.dataset.mcpUiStacked).toBe('true')
|
|
139
|
+
unmount()
|
|
140
|
+
expect(removeListener).toHaveBeenCalledWith('resize', expect.any(Function))
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('keeps nested editor-grid coordinates below 640px while editing', () => {
|
|
144
|
+
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue({ width: 420 } as DOMRect)
|
|
145
|
+
const nested: UIComponent = {
|
|
146
|
+
id: 'editor-nested', type: 'grid', position: { colStart: 1, colSpan: 12 },
|
|
147
|
+
params: { columns: 2, children: [textComponent('editor-left', 1, 1), textComponent('editor-right', 2, 1)] },
|
|
148
|
+
}
|
|
149
|
+
const editableLayout: UILayout = {
|
|
150
|
+
id: 'editor-layout', grid: { columns: 12, gap: '1rem' }, components: [nested],
|
|
151
|
+
}
|
|
152
|
+
const { container } = render(() => <EditableUIResourceRenderer layout={editableLayout} dragDrop={{ enabled: true }} />)
|
|
153
|
+
const nestedGrid = container.querySelector<HTMLElement>(
|
|
154
|
+
'[data-component-type="grid"][data-component-id="editor-nested"] > [data-mcp-ui-grid]'
|
|
155
|
+
)!
|
|
156
|
+
expect(nestedGrid.dataset.mcpUiStacked).toBe('false')
|
|
157
|
+
expect(nestedGrid.style.gridTemplateColumns).toBe('repeat(2, minmax(0, 1fr))')
|
|
158
|
+
expect((nestedGrid.children[1] as HTMLElement).style.gridColumn).toBe('2 / span 1')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('keeps the disabled editor fallback responsive', () => {
|
|
162
|
+
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue({ width: 420 } as DOMRect)
|
|
163
|
+
const { container } = render(() => <EditableUIResourceRenderer layout={layout()} />)
|
|
164
|
+
expect(container.querySelector<HTMLElement>('[data-mcp-ui-grid]')?.dataset.mcpUiStacked).toBe('true')
|
|
165
|
+
})
|
|
166
|
+
})
|