@seed-ship/mcp-ui-solid 4.0.0 → 4.0.1
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/dist/components/DataPreviewSection.cjs +145 -104
- package/dist/components/DataPreviewSection.cjs.map +1 -1
- package/dist/components/DataPreviewSection.d.ts +1 -1
- package/dist/components/DataPreviewSection.d.ts.map +1 -1
- package/dist/components/DataPreviewSection.js +147 -106
- package/dist/components/DataPreviewSection.js.map +1 -1
- package/dist/components/ScratchpadPanel.cjs +9 -1
- package/dist/components/ScratchpadPanel.cjs.map +1 -1
- package/dist/components/ScratchpadPanel.js +9 -1
- package/dist/components/ScratchpadPanel.js.map +1 -1
- package/package.json +1 -1
- package/src/components/DataPreviewSection.tsx +148 -103
- package/src/components/ScratchpadPanel.tsx +9 -1
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DataPreviewSection — paginated data table with export
|
|
3
|
-
*
|
|
3
|
+
* v4.0.1: Fixed rendering — defensive guards for store proxy content
|
|
4
4
|
*
|
|
5
5
|
* @experimental
|
|
6
6
|
*
|
|
@@ -22,7 +22,6 @@ export interface DataPreviewSectionProps {
|
|
|
22
22
|
/** Format a number for display (French locale) */
|
|
23
23
|
function formatNumber(value: unknown, format?: string): string {
|
|
24
24
|
if (typeof value !== 'number' || !isFinite(value)) return String(value ?? '')
|
|
25
|
-
// Simple formatting: use locale
|
|
26
25
|
if (format === 'percent') return `${(value * 100).toFixed(1)}%`
|
|
27
26
|
if (format === 'currency') return `${value.toLocaleString('fr-FR')} EUR`
|
|
28
27
|
if (Number.isInteger(value)) return value.toLocaleString('fr-FR')
|
|
@@ -31,7 +30,7 @@ function formatNumber(value: unknown, format?: string): string {
|
|
|
31
30
|
|
|
32
31
|
/** Format a cell value based on column type */
|
|
33
32
|
function formatCell(value: unknown, col: DataPreviewColumn): string {
|
|
34
|
-
if (value == null) return '
|
|
33
|
+
if (value == null) return '\u2014'
|
|
35
34
|
if (col.type === 'number') return formatNumber(value, col.format)
|
|
36
35
|
if (col.type === 'date' && typeof value === 'string') {
|
|
37
36
|
try {
|
|
@@ -68,26 +67,64 @@ function downloadFile(content: string, filename: string, mimeType: string) {
|
|
|
68
67
|
URL.revokeObjectURL(url)
|
|
69
68
|
}
|
|
70
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Extract a valid DataPreviewContent from props.content.
|
|
72
|
+
* Handles: direct DataPreviewContent, or wrapped in an extra layer.
|
|
73
|
+
*/
|
|
74
|
+
function resolveContent(raw: unknown): DataPreviewContent | null {
|
|
75
|
+
if (!raw || typeof raw !== 'object') return null
|
|
76
|
+
const obj = raw as Record<string, unknown>
|
|
77
|
+
|
|
78
|
+
// Direct shape: { columns: [...], rows: [...] }
|
|
79
|
+
if (Array.isArray(obj.columns) && Array.isArray(obj.rows)) {
|
|
80
|
+
return obj as unknown as DataPreviewContent
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Wrapped shape: { content: { columns: [...], rows: [...] } }
|
|
84
|
+
if (obj.content && typeof obj.content === 'object') {
|
|
85
|
+
const inner = obj.content as Record<string, unknown>
|
|
86
|
+
if (Array.isArray(inner.columns) && Array.isArray(inner.rows)) {
|
|
87
|
+
return inner as unknown as DataPreviewContent
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
|
|
71
94
|
export function DataPreviewSection(props: DataPreviewSectionProps) {
|
|
72
|
-
const content = () =>
|
|
73
|
-
|
|
95
|
+
const content = createMemo(() => {
|
|
96
|
+
const resolved = resolveContent(props.content)
|
|
97
|
+
if (!resolved) {
|
|
98
|
+
console.warn(
|
|
99
|
+
'[MCP-UI] DataPreviewSection: invalid content — expected { columns: [...], rows: [...] }, got:',
|
|
100
|
+
props.content
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
return resolved
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const columns = () => content()?.columns || []
|
|
107
|
+
const rows = () => content()?.rows || []
|
|
108
|
+
const pageSize = () => content()?.pageSize || 25
|
|
74
109
|
const [page, setPage] = createSignal(0)
|
|
75
110
|
|
|
76
|
-
const totalRows = () =>
|
|
111
|
+
const totalRows = () => rows().length
|
|
77
112
|
const totalPages = () => Math.max(1, Math.ceil(totalRows() / pageSize()))
|
|
78
113
|
|
|
79
114
|
const pagedRows = createMemo(() => {
|
|
80
115
|
const start = page() * pageSize()
|
|
81
|
-
return
|
|
116
|
+
return rows().slice(start, start + pageSize())
|
|
82
117
|
})
|
|
83
118
|
|
|
84
119
|
const handleExportCSV = () => {
|
|
85
|
-
const
|
|
120
|
+
const c = content()
|
|
121
|
+
if (!c) return
|
|
122
|
+
const csv = toCSV(c.columns, c.rows)
|
|
86
123
|
downloadFile(csv, 'data-export.csv', 'text/csv;charset=utf-8')
|
|
87
124
|
}
|
|
88
125
|
|
|
89
126
|
const handleExportJSON = () => {
|
|
90
|
-
const json = JSON.stringify(
|
|
127
|
+
const json = JSON.stringify(rows(), null, 2)
|
|
91
128
|
downloadFile(json, 'data-export.json', 'application/json')
|
|
92
129
|
}
|
|
93
130
|
|
|
@@ -98,109 +135,117 @@ export function DataPreviewSection(props: DataPreviewSectionProps) {
|
|
|
98
135
|
}
|
|
99
136
|
|
|
100
137
|
return (
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
<div class="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
|
|
105
|
-
<Show when={content().source}>
|
|
106
|
-
<span class="font-medium">{content().source}</span>
|
|
107
|
-
</Show>
|
|
108
|
-
<Show when={content().freshness}>
|
|
109
|
-
<span class="px-1.5 py-0.5 rounded bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300">
|
|
110
|
-
{content().freshness}
|
|
111
|
-
</span>
|
|
112
|
-
</Show>
|
|
113
|
-
</div>
|
|
114
|
-
|
|
115
|
-
<Show when={content().exportable !== false}>
|
|
116
|
-
<div class="flex items-center gap-1">
|
|
117
|
-
<button
|
|
118
|
-
class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
|
119
|
-
onClick={handleExportCSV}
|
|
120
|
-
title="Export CSV"
|
|
121
|
-
>
|
|
122
|
-
CSV
|
|
123
|
-
</button>
|
|
124
|
-
<button
|
|
125
|
-
class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
|
126
|
-
onClick={handleExportJSON}
|
|
127
|
-
title="Export JSON"
|
|
128
|
-
>
|
|
129
|
-
JSON
|
|
130
|
-
</button>
|
|
131
|
-
</div>
|
|
132
|
-
</Show>
|
|
138
|
+
<Show when={content()} fallback={
|
|
139
|
+
<div class="text-xs text-amber-600 dark:text-amber-400 p-2">
|
|
140
|
+
[DataPreviewSection] Invalid content format
|
|
133
141
|
</div>
|
|
134
|
-
|
|
135
|
-
{
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
<
|
|
139
|
-
<
|
|
140
|
-
<
|
|
141
|
-
{(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
142
|
+
}>
|
|
143
|
+
{(c) => (
|
|
144
|
+
<div class="data-preview-section">
|
|
145
|
+
{/* Header with source + export */}
|
|
146
|
+
<div class="flex items-center justify-between mb-2">
|
|
147
|
+
<div class="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
|
|
148
|
+
<Show when={c().source}>
|
|
149
|
+
<span class="font-medium">{c().source}</span>
|
|
150
|
+
</Show>
|
|
151
|
+
<Show when={c().freshness}>
|
|
152
|
+
<span class="px-1.5 py-0.5 rounded bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300">
|
|
153
|
+
{c().freshness}
|
|
154
|
+
</span>
|
|
155
|
+
</Show>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<Show when={c().exportable !== false}>
|
|
159
|
+
<div class="flex items-center gap-1">
|
|
160
|
+
<button
|
|
161
|
+
class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
|
162
|
+
onClick={handleExportCSV}
|
|
163
|
+
title="Export CSV"
|
|
164
|
+
>
|
|
165
|
+
CSV
|
|
166
|
+
</button>
|
|
167
|
+
<button
|
|
168
|
+
class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
|
169
|
+
onClick={handleExportJSON}
|
|
170
|
+
title="Export JSON"
|
|
158
171
|
>
|
|
159
|
-
|
|
172
|
+
JSON
|
|
173
|
+
</button>
|
|
174
|
+
</div>
|
|
175
|
+
</Show>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
{/* Table */}
|
|
179
|
+
<div class="overflow-x-auto rounded border border-gray-200 dark:border-gray-700">
|
|
180
|
+
<table class="w-full text-sm">
|
|
181
|
+
<thead>
|
|
182
|
+
<tr class="bg-gray-50 dark:bg-gray-800">
|
|
183
|
+
<For each={columns()}>
|
|
160
184
|
{(col) => (
|
|
161
|
-
<
|
|
162
|
-
class="px-3 py-2 text-gray-
|
|
185
|
+
<th
|
|
186
|
+
class="px-3 py-2 font-medium text-xs text-gray-600 dark:text-gray-400 uppercase tracking-wider border-b border-gray-200 dark:border-gray-700"
|
|
163
187
|
style={{ "text-align": columnAlign(col) }}
|
|
164
188
|
>
|
|
165
|
-
{
|
|
166
|
-
</
|
|
189
|
+
{col.label}
|
|
190
|
+
</th>
|
|
167
191
|
)}
|
|
168
192
|
</For>
|
|
169
193
|
</tr>
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
194
|
+
</thead>
|
|
195
|
+
<tbody>
|
|
196
|
+
<For each={pagedRows()}>
|
|
197
|
+
{(row, i) => (
|
|
198
|
+
<tr
|
|
199
|
+
class="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors"
|
|
200
|
+
classList={{ 'bg-gray-25 dark:bg-gray-850': i() % 2 === 1 }}
|
|
201
|
+
>
|
|
202
|
+
<For each={columns()}>
|
|
203
|
+
{(col) => (
|
|
204
|
+
<td
|
|
205
|
+
class="px-3 py-2 text-gray-800 dark:text-gray-200"
|
|
206
|
+
style={{ "text-align": columnAlign(col) }}
|
|
207
|
+
>
|
|
208
|
+
{formatCell(row[col.key], col)}
|
|
209
|
+
</td>
|
|
210
|
+
)}
|
|
211
|
+
</For>
|
|
212
|
+
</tr>
|
|
213
|
+
)}
|
|
214
|
+
</For>
|
|
215
|
+
</tbody>
|
|
216
|
+
</table>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
{/* Footer: pagination + row count */}
|
|
220
|
+
<div class="flex items-center justify-between mt-2 text-xs text-gray-500 dark:text-gray-400">
|
|
221
|
+
<span>
|
|
222
|
+
{c().totalRows
|
|
223
|
+
? `${totalRows()} / ${c().totalRows!.toLocaleString('fr-FR')} rows`
|
|
224
|
+
: `${totalRows()} row${totalRows() !== 1 ? 's' : ''}`}
|
|
225
|
+
</span>
|
|
175
226
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
class="px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-40 transition-colors"
|
|
196
|
-
disabled={page() >= totalPages() - 1}
|
|
197
|
-
onClick={() => setPage(p => p + 1)}
|
|
198
|
-
>
|
|
199
|
-
»
|
|
200
|
-
</button>
|
|
227
|
+
<Show when={totalPages() > 1}>
|
|
228
|
+
<div class="flex items-center gap-1">
|
|
229
|
+
<button
|
|
230
|
+
class="px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-40 transition-colors"
|
|
231
|
+
disabled={page() === 0}
|
|
232
|
+
onClick={() => setPage(p => p - 1)}
|
|
233
|
+
>
|
|
234
|
+
«
|
|
235
|
+
</button>
|
|
236
|
+
<span>{page() + 1} / {totalPages()}</span>
|
|
237
|
+
<button
|
|
238
|
+
class="px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-40 transition-colors"
|
|
239
|
+
disabled={page() >= totalPages() - 1}
|
|
240
|
+
onClick={() => setPage(p => p + 1)}
|
|
241
|
+
>
|
|
242
|
+
»
|
|
243
|
+
</button>
|
|
244
|
+
</div>
|
|
245
|
+
</Show>
|
|
201
246
|
</div>
|
|
202
|
-
</
|
|
203
|
-
|
|
204
|
-
</
|
|
247
|
+
</div>
|
|
248
|
+
)}
|
|
249
|
+
</Show>
|
|
205
250
|
)
|
|
206
251
|
}
|
|
@@ -644,7 +644,15 @@ const ActionSection: Component<{
|
|
|
644
644
|
content: unknown
|
|
645
645
|
onAction?: (action: string, data?: unknown) => void
|
|
646
646
|
}> = (props) => {
|
|
647
|
-
const actions = () =>
|
|
647
|
+
const actions = () => {
|
|
648
|
+
if (Array.isArray(props.content)) return props.content as Array<{ label: string; value?: string; action?: string; variant?: string; icon?: string }>
|
|
649
|
+
const obj = props.content as Record<string, unknown> | null
|
|
650
|
+
if (obj && Array.isArray(obj.actions)) {
|
|
651
|
+
console.warn('[MCP-UI] ActionSection: content should be an array, got { actions: [...] }. Unwrapping automatically.')
|
|
652
|
+
return obj.actions as Array<{ label: string; value?: string; action?: string; variant?: string; icon?: string }>
|
|
653
|
+
}
|
|
654
|
+
return []
|
|
655
|
+
}
|
|
648
656
|
return (
|
|
649
657
|
<div class="flex flex-wrap gap-2">
|
|
650
658
|
<For each={actions()}>
|