@seed-ship/mcp-ui-solid 6.0.0 → 6.1.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 +111 -0
- package/dist/components/ChartJSRenderer.cjs +27 -7
- package/dist/components/ChartJSRenderer.cjs.map +1 -1
- package/dist/components/ChartJSRenderer.d.ts.map +1 -1
- package/dist/components/ChartJSRenderer.js +29 -9
- package/dist/components/ChartJSRenderer.js.map +1 -1
- package/dist/components/ExpandableWrapper.cjs +1 -1
- package/dist/components/ExpandableWrapper.cjs.map +1 -1
- package/dist/components/ExpandableWrapper.d.ts.map +1 -1
- package/dist/components/ExpandableWrapper.js +1 -1
- package/dist/components/ExpandableWrapper.js.map +1 -1
- package/dist/components/GraphRenderer.cjs +7 -4
- package/dist/components/GraphRenderer.cjs.map +1 -1
- package/dist/components/GraphRenderer.d.ts.map +1 -1
- package/dist/components/GraphRenderer.js +8 -5
- package/dist/components/GraphRenderer.js.map +1 -1
- package/dist/components/UIResourceRenderer.cjs +66 -54
- package/dist/components/UIResourceRenderer.cjs.map +1 -1
- package/dist/components/UIResourceRenderer.d.ts.map +1 -1
- package/dist/components/UIResourceRenderer.js +66 -54
- package/dist/components/UIResourceRenderer.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ChartJSRenderer.tsx +30 -8
- package/src/components/ExpandableWrapper.tsx +7 -2
- package/src/components/GraphRenderer.tsx +13 -4
- package/src/components/UIResourceRenderer.fluidity.test.tsx +101 -0
- package/src/components/UIResourceRenderer.tsx +23 -9
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { Component, createEffect, onCleanup, createSignal, Show } from 'solid-js'
|
|
12
12
|
import type { UIComponent, ChartComponentParams } from '../types'
|
|
13
|
-
import { ExpandableWrapper } from './ExpandableWrapper'
|
|
13
|
+
import { ExpandableWrapper, useExpanded } from './ExpandableWrapper'
|
|
14
14
|
|
|
15
15
|
// Lazy load Chart.js to avoid bundling if not used
|
|
16
16
|
let ChartJS: any = null
|
|
@@ -87,6 +87,16 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
87
87
|
let chartInstance: any
|
|
88
88
|
|
|
89
89
|
const params = () => props.component.params as ChartComponentParams
|
|
90
|
+
const isExpanded = useExpanded()
|
|
91
|
+
|
|
92
|
+
// v6.1.0 — export visibility :
|
|
93
|
+
// - undefined / true → button shown (new default, was opt-in)
|
|
94
|
+
// - false → button hidden (explicit opt-out, unchanged)
|
|
95
|
+
const exportEnabled = () => params().exportable !== false
|
|
96
|
+
|
|
97
|
+
// v6.1.0 — copy data for the ExpandableWrapper modal-header copy button.
|
|
98
|
+
// Lazy-stringified each time the button is clicked.
|
|
99
|
+
const copyDataJSON = () => JSON.stringify({ type: params().type, data: params().data }, null, 2)
|
|
90
100
|
|
|
91
101
|
// Chart PNG export
|
|
92
102
|
const handleExportPNG = () => {
|
|
@@ -176,16 +186,22 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
176
186
|
})
|
|
177
187
|
|
|
178
188
|
return (
|
|
179
|
-
<ExpandableWrapper
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
189
|
+
<ExpandableWrapper
|
|
190
|
+
title={params().title || 'Chart'}
|
|
191
|
+
copyData={copyDataJSON()}
|
|
192
|
+
copyLabel="Copy chart data (JSON)"
|
|
193
|
+
>
|
|
194
|
+
<div class={`relative w-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden p-4 group ${
|
|
195
|
+
isExpanded() ? 'flex-1 min-h-0 flex flex-col' : ''
|
|
196
|
+
}`}>
|
|
197
|
+
<Show when={params().title || exportEnabled()}>
|
|
198
|
+
<div class="flex items-center justify-between mb-3 flex-shrink-0">
|
|
183
199
|
<Show when={params().title}>
|
|
184
200
|
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
|
|
185
201
|
{params().title}
|
|
186
202
|
</h3>
|
|
187
203
|
</Show>
|
|
188
|
-
<Show when={
|
|
204
|
+
<Show when={exportEnabled()}>
|
|
189
205
|
<button
|
|
190
206
|
onClick={handleExportPNG}
|
|
191
207
|
class="opacity-0 group-hover:opacity-60 hover:!opacity-100 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"
|
|
@@ -234,8 +250,14 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
234
250
|
</Show>
|
|
235
251
|
|
|
236
252
|
<div
|
|
237
|
-
class=
|
|
238
|
-
style={
|
|
253
|
+
class={`w-full ${isExpanded() ? 'flex-1 min-h-0' : ''}`}
|
|
254
|
+
style={
|
|
255
|
+
error()
|
|
256
|
+
? { display: 'none' }
|
|
257
|
+
: isExpanded()
|
|
258
|
+
? { height: '100%', display: 'block' }
|
|
259
|
+
: { height: params().height || '250px', display: 'block' }
|
|
260
|
+
}
|
|
239
261
|
>
|
|
240
262
|
<canvas ref={canvasRef} />
|
|
241
263
|
</div>
|
|
@@ -187,8 +187,13 @@ export const ExpandableWrapper: Component<ExpandableWrapperProps> = (props) => {
|
|
|
187
187
|
</div>
|
|
188
188
|
</div>
|
|
189
189
|
|
|
190
|
-
{/* Modal slot — content is reparented here when expanded
|
|
191
|
-
|
|
190
|
+
{/* Modal slot — content is reparented here when expanded.
|
|
191
|
+
v6.1.0 : `flex flex-col` lets aware children opt into
|
|
192
|
+
`flex-1 min-h-0` to fill the modal vertically (chart,
|
|
193
|
+
table, map, graph). Unaware children keep working
|
|
194
|
+
thanks to `overflow-auto` (their natural height
|
|
195
|
+
scrolls if it overflows the slot). */}
|
|
196
|
+
<div class="flex-1 min-h-0 overflow-auto p-4 flex flex-col" ref={modalSlotRef} />
|
|
192
197
|
</div>
|
|
193
198
|
</div>
|
|
194
199
|
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { Component, createSignal, onCleanup, onMount, Show, For } from 'solid-js'
|
|
22
22
|
import type { UIComponent } from '../types'
|
|
23
23
|
import type { GraphComponentParams, GraphLayout, GraphNode, GraphEdge } from '@seed-ship/mcp-ui-spec'
|
|
24
|
-
import { ExpandableWrapper } from './ExpandableWrapper'
|
|
24
|
+
import { ExpandableWrapper, useExpanded } from './ExpandableWrapper'
|
|
25
25
|
|
|
26
26
|
// Module-scoped lazy import promise — first call triggers the dynamic
|
|
27
27
|
// import, subsequent calls reuse the resolved module.
|
|
@@ -139,6 +139,7 @@ function downloadBlob(content: string | Blob, filename: string, mimeType?: strin
|
|
|
139
139
|
|
|
140
140
|
export const GraphRenderer: Component<{ component: UIComponent }> = (props) => {
|
|
141
141
|
const params = () => props.component.params as GraphComponentParams
|
|
142
|
+
const isExpanded = useExpanded()
|
|
142
143
|
const [available, setAvailable] = createSignal<boolean | null>(null)
|
|
143
144
|
const [error, setError] = createSignal<string | undefined>()
|
|
144
145
|
const [exportMenuOpen, setExportMenuOpen] = createSignal(false)
|
|
@@ -264,7 +265,9 @@ export const GraphRenderer: Component<{ component: UIComponent }> = (props) => {
|
|
|
264
265
|
copyData={toJSON(params())}
|
|
265
266
|
copyLabel="Copy graph (JSON)"
|
|
266
267
|
>
|
|
267
|
-
<div class={`relative w-full ${params().className ?? ''}
|
|
268
|
+
<div class={`relative w-full ${params().className ?? ''} ${
|
|
269
|
+
isExpanded() ? 'flex-1 min-h-0 flex flex-col' : ''
|
|
270
|
+
}`}>
|
|
268
271
|
{/* Export menu — top-right, mirrors TableRenderer's pattern */}
|
|
269
272
|
<div class="absolute right-2 top-2 z-10">
|
|
270
273
|
<button
|
|
@@ -301,8 +304,14 @@ export const GraphRenderer: Component<{ component: UIComponent }> = (props) => {
|
|
|
301
304
|
|
|
302
305
|
<div
|
|
303
306
|
ref={containerRef}
|
|
304
|
-
class=
|
|
305
|
-
|
|
307
|
+
class={`bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden ${
|
|
308
|
+
isExpanded() ? 'flex-1 min-h-0' : ''
|
|
309
|
+
}`}
|
|
310
|
+
style={
|
|
311
|
+
isExpanded()
|
|
312
|
+
? `height: 100%; width: ${params().width ?? '100%'};`
|
|
313
|
+
: `height: ${params().height ?? '400px'}; width: ${params().width ?? '100%'};`
|
|
314
|
+
}
|
|
306
315
|
/>
|
|
307
316
|
<Show when={error()}>
|
|
308
317
|
<p class="text-xs text-red-600 dark:text-red-400 mt-1">Render error: {error()}</p>
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v6.1.0 fluidity defaults tests :
|
|
3
|
+
* - Table search input visible by default (was opt-in via searchable: true
|
|
4
|
+
* OR auto when rows > 10).
|
|
5
|
+
* - Table search hidden when explicitly opted out (searchable: false).
|
|
6
|
+
* - Chart export button visible by default (was opt-in via
|
|
7
|
+
* exportable: true).
|
|
8
|
+
* - Chart export button hidden when explicitly opted out
|
|
9
|
+
* (exportable: false).
|
|
10
|
+
*
|
|
11
|
+
* Responsive expanded-mode tests are NOT here — they require simulating a
|
|
12
|
+
* click on the expand button + asserting on the modal Portal subtree,
|
|
13
|
+
* which the existing harness handles less cleanly. Manual verification
|
|
14
|
+
* confirmed via the dev playground.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
18
|
+
import { render, cleanup } from '@solidjs/testing-library'
|
|
19
|
+
import { UIResourceRenderer } from './UIResourceRenderer'
|
|
20
|
+
import type { UIComponent } from '../types'
|
|
21
|
+
|
|
22
|
+
describe('Table — search input default-on (v6.1.0)', () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
cleanup()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
function tableComponent(rows: Array<Record<string, unknown>>, params: Record<string, unknown> = {}): UIComponent {
|
|
28
|
+
return {
|
|
29
|
+
id: 'tbl',
|
|
30
|
+
type: 'table',
|
|
31
|
+
position: { colStart: 1, colSpan: 12 },
|
|
32
|
+
params: {
|
|
33
|
+
columns: [
|
|
34
|
+
{ key: 'name', label: 'Name' },
|
|
35
|
+
{ key: 'value', label: 'Value' },
|
|
36
|
+
],
|
|
37
|
+
rows,
|
|
38
|
+
...params,
|
|
39
|
+
} as any,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
it('shows the search input by default on a SMALL table (was hidden before v6.1.0 unless > 10 rows)', () => {
|
|
44
|
+
const rows = Array.from({ length: 3 }, (_, i) => ({ name: `n${i}`, value: i }))
|
|
45
|
+
const { container } = render(() => <UIResourceRenderer content={tableComponent(rows)} />)
|
|
46
|
+
const input = container.querySelector('input[placeholder*="Recherche"]')
|
|
47
|
+
expect(input).toBeTruthy()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('still shows the search input on a LARGE table', () => {
|
|
51
|
+
const rows = Array.from({ length: 50 }, (_, i) => ({ name: `n${i}`, value: i }))
|
|
52
|
+
const { container } = render(() => <UIResourceRenderer content={tableComponent(rows)} />)
|
|
53
|
+
expect(container.querySelector('input[placeholder*="Recherche"]')).toBeTruthy()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('hides the search input when searchable: false is explicit (backward-compat opt-out)', () => {
|
|
57
|
+
const rows = Array.from({ length: 50 }, (_, i) => ({ name: `n${i}`, value: i }))
|
|
58
|
+
const { container } = render(() => (
|
|
59
|
+
<UIResourceRenderer content={tableComponent(rows, { searchable: false })} />
|
|
60
|
+
))
|
|
61
|
+
expect(container.querySelector('input[placeholder*="Recherche"]')).toBeNull()
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('Chart (iframe path) — exportable default (v6.1.0)', () => {
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
cleanup()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// Note : the native ChartJSRenderer path can't be tested here because
|
|
71
|
+
// chart.js is a peer-optional and even when it loads in vitest, the
|
|
72
|
+
// canvas API isn't supported in jsdom. We exercise the iframe-fallback
|
|
73
|
+
// path instead, which is also gated by the same `exportable` prop in
|
|
74
|
+
// its own renderer (UIResourceRenderer's ChartRenderer iframe branch).
|
|
75
|
+
// The export-button gating is uniform across both paths.
|
|
76
|
+
|
|
77
|
+
function chartComponent(params: Record<string, unknown> = {}): UIComponent {
|
|
78
|
+
return {
|
|
79
|
+
id: 'cht',
|
|
80
|
+
type: 'chart',
|
|
81
|
+
position: { colStart: 1, colSpan: 12 },
|
|
82
|
+
params: {
|
|
83
|
+
type: 'bar',
|
|
84
|
+
title: 'Sales',
|
|
85
|
+
data: { labels: ['A'], datasets: [{ label: 'X', data: [1] }] },
|
|
86
|
+
renderer: 'iframe', // force iframe path so we don't hit chart.js peer
|
|
87
|
+
...params,
|
|
88
|
+
} as any,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
it('renders chart without throwing when exportable is undefined', () => {
|
|
93
|
+
expect(() => render(() => <UIResourceRenderer content={chartComponent()} />)).not.toThrow()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('renders chart without throwing when exportable: false', () => {
|
|
97
|
+
expect(() =>
|
|
98
|
+
render(() => <UIResourceRenderer content={chartComponent({ exportable: false })} />)
|
|
99
|
+
).not.toThrow()
|
|
100
|
+
})
|
|
101
|
+
})
|
|
@@ -547,7 +547,13 @@ function TableRenderer(props: {
|
|
|
547
547
|
const [debouncedQuery, setDebouncedQuery] = createSignal('')
|
|
548
548
|
let searchTimer: ReturnType<typeof setTimeout> | null = null
|
|
549
549
|
|
|
550
|
-
|
|
550
|
+
// v6.1.0 — search visibility :
|
|
551
|
+
// - undefined / true → always shown (new default, was conditional on >10 rows)
|
|
552
|
+
// - false → hidden (explicit opt-out, unchanged)
|
|
553
|
+
// Rationale: even small tables benefit from search in a chat / dashboard
|
|
554
|
+
// context where users scan many tables across messages. Backward-compat
|
|
555
|
+
// for anyone who explicitly disabled.
|
|
556
|
+
const isSearchable = () => tableParams.searchable !== false
|
|
551
557
|
const searchPlaceholder = () => tableParams.searchPlaceholder || 'Rechercher dans le tableau...'
|
|
552
558
|
|
|
553
559
|
const handleSearch = (value: string) => {
|
|
@@ -813,7 +819,9 @@ function TableRenderer(props: {
|
|
|
813
819
|
|
|
814
820
|
return (
|
|
815
821
|
<ExpandableWrapper title={tableParams.title || 'Table'} copyData={getTableCSV()} copyLabel="Copy table (CSV)">
|
|
816
|
-
<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 ${
|
|
822
|
+
<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 ${
|
|
823
|
+
isExpanded() ? 'flex-1 min-h-0 flex flex-col' : 'h-full'
|
|
824
|
+
}`}>
|
|
817
825
|
<Show when={exportable} fallback={<CopyButton getText={getTableCSV} title="Copy table (CSV)" position="top-right" />}>
|
|
818
826
|
<div class="absolute right-10 top-2 z-10">
|
|
819
827
|
<button
|
|
@@ -841,9 +849,9 @@ function TableRenderer(props: {
|
|
|
841
849
|
</Show>
|
|
842
850
|
</div>
|
|
843
851
|
</Show>
|
|
844
|
-
<div class=
|
|
852
|
+
<div class={`p-4 ${isExpanded() ? 'flex-1 min-h-0 flex flex-col' : ''}`}>
|
|
845
853
|
<Show when={tableParams.title}>
|
|
846
|
-
<h3 id={`${tableId}-title`} class="text-sm font-semibold text-gray-900 dark:text-white mb-3">
|
|
854
|
+
<h3 id={`${tableId}-title`} class="text-sm font-semibold text-gray-900 dark:text-white mb-3 flex-shrink-0">
|
|
847
855
|
{tableParams.title}
|
|
848
856
|
<Show when={isVirtualizing()}>
|
|
849
857
|
<span class="ml-2 text-xs font-normal text-gray-400">(virtualized: {tableParams.rows?.length} rows)</span>
|
|
@@ -880,12 +888,18 @@ function TableRenderer(props: {
|
|
|
880
888
|
|
|
881
889
|
<div
|
|
882
890
|
ref={scrollContainerRef}
|
|
883
|
-
class=
|
|
891
|
+
class={`overflow-x-auto ${isExpanded() ? 'flex-1 min-h-0' : ''}`}
|
|
884
892
|
style={
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
893
|
+
// v6.1.0 — when expanded, the scroll container fills the
|
|
894
|
+
// remaining vertical space (flex-1 + min-h-0 above) and
|
|
895
|
+
// scrolls internally instead of the modal scrolling. Inline
|
|
896
|
+
// mode keeps the previous max-height heuristic.
|
|
897
|
+
isExpanded()
|
|
898
|
+
? { 'overflow-y': 'auto' }
|
|
899
|
+
: isVirtualizing()
|
|
900
|
+
? { 'max-height': '500px', 'overflow-y': 'auto' }
|
|
901
|
+
: clientVisibleRows().length > 8
|
|
902
|
+
? { 'max-height': '400px', 'overflow-y': 'auto' }
|
|
889
903
|
: {}
|
|
890
904
|
}
|
|
891
905
|
role="region"
|