@seed-ship/mcp-ui-solid 6.24.0 → 6.25.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/CHANGELOG.md +42 -0
- package/README.md +47 -0
- package/dist/components/ActionGroupRenderer.cjs +6 -6
- package/dist/components/ActionGroupRenderer.cjs.map +1 -1
- package/dist/components/ActionGroupRenderer.js +6 -6
- package/dist/components/ActionGroupRenderer.js.map +1 -1
- package/dist/components/AutocompleteDropdown.cjs +6 -6
- package/dist/components/AutocompleteDropdown.cjs.map +1 -1
- package/dist/components/AutocompleteDropdown.js +6 -6
- package/dist/components/AutocompleteDropdown.js.map +1 -1
- package/dist/components/AutocompleteFormField.cjs +6 -5
- package/dist/components/AutocompleteFormField.cjs.map +1 -1
- package/dist/components/AutocompleteFormField.d.ts.map +1 -1
- package/dist/components/AutocompleteFormField.js +6 -5
- package/dist/components/AutocompleteFormField.js.map +1 -1
- package/dist/components/ChartJSRenderer.cjs +216 -6
- package/dist/components/ChartJSRenderer.cjs.map +1 -1
- package/dist/components/ChartJSRenderer.d.ts.map +1 -1
- package/dist/components/ChartJSRenderer.js +217 -7
- package/dist/components/ChartJSRenderer.js.map +1 -1
- package/dist/components/ChatPrompt.cjs +5 -5
- package/dist/components/ChatPrompt.cjs.map +1 -1
- package/dist/components/ChatPrompt.js +5 -5
- package/dist/components/ChatPrompt.js.map +1 -1
- package/dist/components/ComponentToolbar.cjs +5 -5
- package/dist/components/ComponentToolbar.cjs.map +1 -1
- package/dist/components/ComponentToolbar.js +5 -5
- package/dist/components/ComponentToolbar.js.map +1 -1
- package/dist/components/DegradedFallback.cjs +1 -1
- package/dist/components/DegradedFallback.cjs.map +1 -1
- package/dist/components/DegradedFallback.js +1 -1
- package/dist/components/DegradedFallback.js.map +1 -1
- package/dist/components/ExpandableWrapper.cjs +10 -7
- package/dist/components/ExpandableWrapper.cjs.map +1 -1
- package/dist/components/ExpandableWrapper.d.ts.map +1 -1
- package/dist/components/ExpandableWrapper.js +10 -7
- package/dist/components/ExpandableWrapper.js.map +1 -1
- package/dist/components/FormFieldRenderer.cjs +13 -12
- package/dist/components/FormFieldRenderer.cjs.map +1 -1
- package/dist/components/FormFieldRenderer.d.ts.map +1 -1
- package/dist/components/FormFieldRenderer.js +13 -12
- package/dist/components/FormFieldRenderer.js.map +1 -1
- package/dist/components/FormRenderer.cjs +1 -1
- package/dist/components/FormRenderer.cjs.map +1 -1
- package/dist/components/FormRenderer.js +1 -1
- package/dist/components/FormRenderer.js.map +1 -1
- package/dist/components/GenerativeUIErrorBoundary.cjs +1 -1
- package/dist/components/GenerativeUIErrorBoundary.cjs.map +1 -1
- package/dist/components/GenerativeUIErrorBoundary.js +1 -1
- package/dist/components/GenerativeUIErrorBoundary.js.map +1 -1
- package/dist/components/GhostText.cjs +2 -2
- package/dist/components/GhostText.cjs.map +1 -1
- package/dist/components/GhostText.js +2 -2
- package/dist/components/GhostText.js.map +1 -1
- package/dist/components/GraphRenderer.cjs +100 -29
- package/dist/components/GraphRenderer.cjs.map +1 -1
- package/dist/components/GraphRenderer.d.ts +1 -1
- package/dist/components/GraphRenderer.d.ts.map +1 -1
- package/dist/components/GraphRenderer.js +100 -29
- package/dist/components/GraphRenderer.js.map +1 -1
- package/dist/components/MissingValue.cjs +2 -2
- package/dist/components/MissingValue.cjs.map +1 -1
- package/dist/components/MissingValue.js +2 -2
- package/dist/components/MissingValue.js.map +1 -1
- package/dist/components/ModalRenderer.cjs +39 -31
- package/dist/components/ModalRenderer.cjs.map +1 -1
- package/dist/components/ModalRenderer.d.ts.map +1 -1
- package/dist/components/ModalRenderer.js +40 -32
- package/dist/components/ModalRenderer.js.map +1 -1
- package/dist/components/PortalDropdownMenu.cjs +6 -3
- package/dist/components/PortalDropdownMenu.cjs.map +1 -1
- package/dist/components/PortalDropdownMenu.d.ts.map +1 -1
- package/dist/components/PortalDropdownMenu.js +7 -4
- package/dist/components/PortalDropdownMenu.js.map +1 -1
- package/dist/components/ScratchpadPanel.cjs +38 -44
- package/dist/components/ScratchpadPanel.cjs.map +1 -1
- package/dist/components/ScratchpadPanel.d.ts.map +1 -1
- package/dist/components/ScratchpadPanel.js +38 -44
- package/dist/components/ScratchpadPanel.js.map +1 -1
- package/dist/components/UIResourceRenderer.cjs +20 -20
- package/dist/components/UIResourceRenderer.cjs.map +1 -1
- package/dist/components/UIResourceRenderer.js +20 -20
- package/dist/components/UIResourceRenderer.js.map +1 -1
- package/dist/components/chart-missing-plugin.cjs +4 -4
- package/dist/components/chart-missing-plugin.cjs.map +1 -1
- package/dist/components/chart-missing-plugin.d.ts +3 -1
- package/dist/components/chart-missing-plugin.d.ts.map +1 -1
- package/dist/components/chart-missing-plugin.js +4 -4
- package/dist/components/chart-missing-plugin.js.map +1 -1
- package/dist/utils/math-markdown.cjs +1 -1
- package/dist/utils/math-markdown.cjs.map +1 -1
- package/dist/utils/math-markdown.js +1 -1
- package/dist/utils/math-markdown.js.map +1 -1
- package/dist/utils/theme.cjs +91 -0
- package/dist/utils/theme.cjs.map +1 -0
- package/dist/utils/theme.d.ts +11 -0
- package/dist/utils/theme.d.ts.map +1 -0
- package/dist/utils/theme.js +91 -0
- package/dist/utils/theme.js.map +1 -0
- package/docs/SECURITY-DEPENDENCIES.md +81 -0
- package/docs/THEMING.md +205 -0
- package/package.json +5 -3
- package/src/components/ActionGroupRenderer.tsx +6 -6
- package/src/components/AutocompleteDropdown.tsx +17 -17
- package/src/components/AutocompleteFormField.tsx +10 -9
- package/src/components/ChartJSRenderer.tsx +483 -239
- package/src/components/ChatPrompt.tsx +16 -16
- package/src/components/ComponentToolbar.tsx +5 -5
- package/src/components/DegradedFallback.tsx +9 -9
- package/src/components/ExpandableWrapper.tsx +15 -11
- package/src/components/FormFieldRenderer.tsx +52 -51
- package/src/components/FormRenderer.tsx +12 -12
- package/src/components/GenerativeUIErrorBoundary.tsx +6 -6
- package/src/components/GhostText.tsx +7 -7
- package/src/components/GraphRenderer.tsx +83 -67
- package/src/components/MissingValue.tsx +2 -2
- package/src/components/ModalRenderer.tsx +11 -5
- package/src/components/PortalDropdownMenu.tsx +6 -2
- package/src/components/ScratchpadPanel.tsx +144 -141
- package/src/components/UIResourceRenderer.tsx +101 -101
- package/src/components/chart-missing-plugin.ts +5 -3
- package/src/styles/autocomplete.css +53 -53
- package/src/utils/math-markdown.ts +1 -1
- package/src/utils/theme.ts +101 -0
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
createMemo,
|
|
25
25
|
createUniqueId,
|
|
26
26
|
onCleanup,
|
|
27
|
+
onMount,
|
|
27
28
|
createSignal,
|
|
28
29
|
untrack,
|
|
29
30
|
Show,
|
|
@@ -46,6 +47,214 @@ import { PART_TO_WHOLE_CHART_TYPES, summarizeChartMissing } from '../utils/missi
|
|
|
46
47
|
import { useTelemetry } from '../context/MCPUITelemetryContext';
|
|
47
48
|
import { useMCPUIConfig } from '../context/MCPUIConfigContext';
|
|
48
49
|
import { formatMCPUIString, useMCPUIStrings } from '../context/MCPUIStringsContext';
|
|
50
|
+
import { observeTheme, readThemeTokens } from '../utils/theme';
|
|
51
|
+
|
|
52
|
+
const CHART_THEME_TOKENS = [
|
|
53
|
+
...Array.from({ length: 8 }, (_, i) => `--mcp-ui-chart-${i + 1}`),
|
|
54
|
+
'--mcp-ui-chart-grid',
|
|
55
|
+
'--mcp-ui-chart-axis',
|
|
56
|
+
'--mcp-ui-chart-label',
|
|
57
|
+
'--mcp-ui-surface', '--mcp-ui-fg', '--mcp-ui-border',
|
|
58
|
+
] as const;
|
|
59
|
+
|
|
60
|
+
/** A per-instance adapter: never changes Chart.defaults or the caller's data. */
|
|
61
|
+
function createChartTheme(
|
|
62
|
+
params: ChartComponentParams,
|
|
63
|
+
baseOptions: any,
|
|
64
|
+
root: HTMLElement,
|
|
65
|
+
chartConfig: any
|
|
66
|
+
) {
|
|
67
|
+
let tokens = readThemeTokens(root, CHART_THEME_TOKENS);
|
|
68
|
+
const originalDatasets = params.data.datasets.map((dataset) => ({ ...dataset }));
|
|
69
|
+
let engineColors: Array<{ backgroundColor: any; borderColor: any }> | undefined;
|
|
70
|
+
const sharedPaintOptions = new WeakMap<object, any>();
|
|
71
|
+
const paths: Array<{ path: string[]; token: string }> = [
|
|
72
|
+
{ path: ['plugins', 'legend', 'labels', 'color'], token: '--mcp-ui-chart-label' },
|
|
73
|
+
{ path: ['plugins', 'tooltip', 'backgroundColor'], token: '--mcp-ui-surface' },
|
|
74
|
+
{ path: ['plugins', 'tooltip', 'titleColor'], token: '--mcp-ui-fg' },
|
|
75
|
+
{ path: ['plugins', 'tooltip', 'bodyColor'], token: '--mcp-ui-fg' },
|
|
76
|
+
{ path: ['plugins', 'tooltip', 'footerColor'], token: '--mcp-ui-fg' },
|
|
77
|
+
{ path: ['plugins', 'tooltip', 'borderColor'], token: '--mcp-ui-border' },
|
|
78
|
+
];
|
|
79
|
+
const configuredScales = Object.keys(baseOptions.scales ?? {});
|
|
80
|
+
const scales = configuredScales.length ? configuredScales :
|
|
81
|
+
params.type === 'radar' || params.type === 'polarArea' ? ['r'] :
|
|
82
|
+
params.type === 'pie' || params.type === 'doughnut' ? [] : ['x', 'y'];
|
|
83
|
+
const knownScales = new Set<string>();
|
|
84
|
+
const addScalePaths = (names: string[]) => { for (const scale of names) {
|
|
85
|
+
if (knownScales.has(scale)) continue;
|
|
86
|
+
knownScales.add(scale);
|
|
87
|
+
for (const [part, token] of [
|
|
88
|
+
['grid', '--mcp-ui-chart-grid'],
|
|
89
|
+
['border', '--mcp-ui-chart-axis'],
|
|
90
|
+
['ticks', '--mcp-ui-chart-label'],
|
|
91
|
+
['title', '--mcp-ui-chart-label'],
|
|
92
|
+
...(scale === 'r'
|
|
93
|
+
? [
|
|
94
|
+
['angleLines', '--mcp-ui-chart-grid'],
|
|
95
|
+
['pointLabels', '--mcp-ui-chart-label'],
|
|
96
|
+
]
|
|
97
|
+
: []),
|
|
98
|
+
])
|
|
99
|
+
paths.push({ path: ['scales', scale, part, 'color'], token });
|
|
100
|
+
} };
|
|
101
|
+
addScalePaths(scales);
|
|
102
|
+
const read = (value: any, path: string[]) => path.reduce((part, key) => part?.[key], value);
|
|
103
|
+
const owned = new Set<string>();
|
|
104
|
+
const applyOptions = (options: any) => {
|
|
105
|
+
for (const { path, token } of paths) {
|
|
106
|
+
// Explicit caller options, including plugin/scale opt-outs, always win.
|
|
107
|
+
if (read(baseOptions, path) !== undefined) continue;
|
|
108
|
+
if (token === '--mcp-ui-chart-label' && (params.options as any)?.color !== undefined)
|
|
109
|
+
continue;
|
|
110
|
+
let blocked = false;
|
|
111
|
+
for (let i = 1; i < path.length; i++) {
|
|
112
|
+
const part = read(baseOptions, path.slice(0, i));
|
|
113
|
+
if (part === false || part === null) blocked = true;
|
|
114
|
+
}
|
|
115
|
+
if (blocked) continue;
|
|
116
|
+
const key = path.join('.');
|
|
117
|
+
const color = tokens[token];
|
|
118
|
+
if (!color && !owned.has(key)) continue;
|
|
119
|
+
let target = options;
|
|
120
|
+
for (const part of path.slice(0, -1)) {
|
|
121
|
+
// Copy along the changed path only, retaining untouched caller branches.
|
|
122
|
+
target[part] = { ...target[part] };
|
|
123
|
+
target = target[part];
|
|
124
|
+
}
|
|
125
|
+
if (color) {
|
|
126
|
+
target[path.at(-1)!] = color;
|
|
127
|
+
owned.add(key);
|
|
128
|
+
} else {
|
|
129
|
+
delete target[path.at(-1)!];
|
|
130
|
+
owned.delete(key);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
const plugin = {
|
|
135
|
+
id: 'mcp-ui-theme',
|
|
136
|
+
beforeInit(chart: any) {
|
|
137
|
+
// Config has normalized implicit/default axes now. Never invent x/y
|
|
138
|
+
// beside a caller's named axes just to give them a color.
|
|
139
|
+
addScalePaths(Object.keys(chart.config.options.scales ?? {}));
|
|
140
|
+
applyOptions(chart.config.options);
|
|
141
|
+
},
|
|
142
|
+
// Global Colors runs first. Capture its actual output rather than copying
|
|
143
|
+
// a version-specific 7-color palette (or changing its opt-out semantics).
|
|
144
|
+
beforeLayout(chart: any) {
|
|
145
|
+
const datasets = chart.data.datasets;
|
|
146
|
+
engineColors ??= datasets.map((dataset: any) => ({
|
|
147
|
+
backgroundColor: Array.isArray(dataset.backgroundColor)
|
|
148
|
+
? [...dataset.backgroundColor]
|
|
149
|
+
: dataset.backgroundColor,
|
|
150
|
+
borderColor: Array.isArray(dataset.borderColor)
|
|
151
|
+
? [...dataset.borderColor]
|
|
152
|
+
: dataset.borderColor,
|
|
153
|
+
}));
|
|
154
|
+
const callerOptions = params.options as any;
|
|
155
|
+
let pointOffset = 0;
|
|
156
|
+
datasets.forEach((dataset: any, index: number) => {
|
|
157
|
+
const original = originalDatasets[index];
|
|
158
|
+
const baseline = engineColors![index];
|
|
159
|
+
if (!original || !baseline) return;
|
|
160
|
+
const datasetType = (original as any).type ?? params.type;
|
|
161
|
+
const perPoint = ['pie', 'doughnut', 'polarArea'].includes(datasetType);
|
|
162
|
+
const lineWithPoints = datasetType === 'line' || datasetType === 'radar';
|
|
163
|
+
const elementKinds = datasetType === 'bar' ? ['bar'] : perPoint ? ['arc'] :
|
|
164
|
+
lineWithPoints ? ['line'] : ['point'];
|
|
165
|
+
const hasSeriesToken = perPoint
|
|
166
|
+
? dataset.data.some((_: unknown, i: number) => tokens[`--mcp-ui-chart-${((pointOffset + i) % 8) + 1}`])
|
|
167
|
+
: Boolean(tokens[`--mcp-ui-chart-${(index % 8) + 1}`]);
|
|
168
|
+
for (const field of ['backgroundColor', 'borderColor'] as const) {
|
|
169
|
+
if (original[field] !== undefined) continue;
|
|
170
|
+
const callerDatasetColor = callerOptions?.datasets?.[datasetType]?.[field];
|
|
171
|
+
if (callerDatasetColor !== undefined) {
|
|
172
|
+
// Chart.js's global Colors plugin does not check options.datasets
|
|
173
|
+
// and can mask this explicit paint with its generated dataset field.
|
|
174
|
+
// Retain the caller paint when adapting this palette slot, but keep
|
|
175
|
+
// the engine baseline unchanged when no series token is requested.
|
|
176
|
+
const color = hasSeriesToken ? callerDatasetColor : baseline[field];
|
|
177
|
+
if (color !== undefined) dataset[field] = color;
|
|
178
|
+
else delete dataset[field];
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
// A caller's border must not suppress a host fill (or vice versa),
|
|
182
|
+
// nor may options for an unrelated chart/element type mask this one.
|
|
183
|
+
if (callerOptions?.[field] !== undefined ||
|
|
184
|
+
elementKinds.some((kind) => callerOptions?.elements?.[kind]?.[field] !== undefined)) continue;
|
|
185
|
+
if (lineWithPoints && hasSeriesToken) {
|
|
186
|
+
// Dataset paints also feed point defaults. Preserve an explicit
|
|
187
|
+
// point paint under its native prefix before theming the line;
|
|
188
|
+
// otherwise the generic themed field would outrank elements.point.
|
|
189
|
+
const pointField = field === 'borderColor' ? 'pointBorderColor' : 'pointBackgroundColor';
|
|
190
|
+
const scopedPointPaint = callerOptions?.datasets?.[datasetType]?.elements?.point?.[field];
|
|
191
|
+
const pointPaint = scopedPointPaint !== undefined ? scopedPointPaint :
|
|
192
|
+
callerOptions?.elements?.point?.[field];
|
|
193
|
+
if ((original as any)[pointField] === undefined &&
|
|
194
|
+
callerOptions?.datasets?.[datasetType]?.[pointField] === undefined &&
|
|
195
|
+
callerOptions?.[pointField] === undefined && pointPaint !== undefined) {
|
|
196
|
+
dataset[pointField] = pointPaint;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Pie-like charts have no automatic per-point border palette.
|
|
200
|
+
if (perPoint && field === 'borderColor') continue;
|
|
201
|
+
if (perPoint) {
|
|
202
|
+
const previous = baseline[field];
|
|
203
|
+
const colors = dataset.data.map(
|
|
204
|
+
(_: unknown, i: number) =>
|
|
205
|
+
tokens[`--mcp-ui-chart-${((pointOffset + i) % 8) + 1}`] ??
|
|
206
|
+
(Array.isArray(previous) ? previous[i % previous.length] : previous)
|
|
207
|
+
);
|
|
208
|
+
dataset[field] = colors.some(
|
|
209
|
+
(_: unknown, i: number) => tokens[`--mcp-ui-chart-${((pointOffset + i) % 8) + 1}`]
|
|
210
|
+
)
|
|
211
|
+
? colors
|
|
212
|
+
: previous;
|
|
213
|
+
} else {
|
|
214
|
+
const color = tokens[`--mcp-ui-chart-${(index % 8) + 1}`];
|
|
215
|
+
if (color || baseline[field] !== undefined) dataset[field] = color ?? baseline[field];
|
|
216
|
+
else delete dataset[field];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (perPoint) pointOffset += dataset.data.length;
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
afterDatasetUpdate(_chart: any, args: any) {
|
|
223
|
+
const meta = args.meta;
|
|
224
|
+
if (!meta?.data?.length || typeof meta.controller?.getStyle !== 'function') return;
|
|
225
|
+
const index = meta.data.findIndex((element: any) => element.options?.$shared && !element.active);
|
|
226
|
+
if (index >= 0 && !Object.isFrozen(meta.data[index].options)) {
|
|
227
|
+
sharedPaintOptions.set(meta.controller, meta.data[index].options);
|
|
228
|
+
}
|
|
229
|
+
if (args.mode !== 'none') return;
|
|
230
|
+
// Chart.js deliberately skips updateSharedOptions in direct ('none')
|
|
231
|
+
// mode. Refresh only its shared paint values from the public resolver;
|
|
232
|
+
// retain the object captured before hover, even when every element is
|
|
233
|
+
// active now, so later deactivation cannot revive an old theme paint.
|
|
234
|
+
const shared = sharedPaintOptions.get(meta.controller);
|
|
235
|
+
if (!shared) return;
|
|
236
|
+
const resolved = meta.controller.getStyle(index >= 0 ? index : 0, false);
|
|
237
|
+
if (!resolved.$shared) return;
|
|
238
|
+
shared.backgroundColor = resolved.backgroundColor;
|
|
239
|
+
shared.borderColor = resolved.borderColor;
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
// Bookkeeping must start before the first hover, even without host tokens.
|
|
243
|
+
// Without that capture a theme first enabled while all points are active
|
|
244
|
+
// cannot reach the native shared options using public APIs alone.
|
|
245
|
+
chartConfig.plugins = [...(chartConfig.plugins ?? []), plugin];
|
|
246
|
+
return {
|
|
247
|
+
applyOptions,
|
|
248
|
+
refresh(chart: any) {
|
|
249
|
+
const next = readThemeTokens(root, CHART_THEME_TOKENS);
|
|
250
|
+
if (CHART_THEME_TOKENS.every((name) => next[name] === tokens[name])) return;
|
|
251
|
+
tokens = next;
|
|
252
|
+
addScalePaths(Object.keys(chart.scales ?? chart.config.options.scales ?? {}));
|
|
253
|
+
applyOptions(chart.config.options);
|
|
254
|
+
chart.update('none');
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
49
258
|
|
|
50
259
|
// Lazy load Chart.js to avoid bundling if not used
|
|
51
260
|
let ChartJS: any = null;
|
|
@@ -315,9 +524,20 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
315
524
|
const [zoomPlugin, setZoomPlugin] = createSignal<unknown | null>(null);
|
|
316
525
|
const [isZoomed, setIsZoomed] = createSignal(false);
|
|
317
526
|
let canvasRef: HTMLCanvasElement | undefined;
|
|
527
|
+
let themeRoot: HTMLDivElement | undefined;
|
|
318
528
|
let chartInstance: any;
|
|
529
|
+
let chartTheme: ReturnType<typeof createChartTheme> | undefined;
|
|
319
530
|
let renderVersion = 0;
|
|
320
531
|
|
|
532
|
+
onMount(() => {
|
|
533
|
+
if (themeRoot) {
|
|
534
|
+
const stop = observeTheme(themeRoot, () => {
|
|
535
|
+
if (chartInstance) chartTheme?.refresh(chartInstance);
|
|
536
|
+
});
|
|
537
|
+
onCleanup(stop);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
321
541
|
const params = () => props.component.params as ChartComponentParams;
|
|
322
542
|
const telemetry = useTelemetry();
|
|
323
543
|
const config = useMCPUIConfig();
|
|
@@ -458,8 +678,7 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
458
678
|
* here (host policy × surface × chart type, and the peer actually loaded),
|
|
459
679
|
* and the chart is what is currently being shown.
|
|
460
680
|
*/
|
|
461
|
-
const zoomControlsShown = () =>
|
|
462
|
-
Boolean(zoomOptions()) && activeView() === 'chart' && !error();
|
|
681
|
+
const zoomControlsShown = () => Boolean(zoomOptions()) && activeView() === 'chart' && !error();
|
|
463
682
|
|
|
464
683
|
/**
|
|
465
684
|
* Zoom the chart by a fixed ratio, about its centre.
|
|
@@ -585,7 +804,10 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
585
804
|
|
|
586
805
|
const chartConfig: any = {
|
|
587
806
|
type: chartParams.type,
|
|
588
|
-
|
|
807
|
+
// The Colors plugin writes its generated palette onto datasets. Never
|
|
808
|
+
// let that turn a shared payload into caller-supplied colors for the
|
|
809
|
+
// next renderer, nor mutate data owned by a host application.
|
|
810
|
+
data: { ...chartParams.data, datasets: chartParams.data.datasets.map((dataset) => ({ ...dataset })) },
|
|
589
811
|
options: baseOptions,
|
|
590
812
|
};
|
|
591
813
|
|
|
@@ -636,10 +858,24 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
636
858
|
allMissing: chartMissing.allMissing,
|
|
637
859
|
getNoDataText: () => strings.chartNoData,
|
|
638
860
|
getFontFamily: () => Chart?.defaults?.font?.family,
|
|
861
|
+
getTextColor: () => (chartParams.options as any)?.color ??
|
|
862
|
+
readThemeTokens(themeRoot, ['--mcp-ui-chart-label'])['--mcp-ui-chart-label'],
|
|
639
863
|
}),
|
|
640
864
|
];
|
|
641
865
|
}
|
|
642
866
|
|
|
867
|
+
if (themeRoot) {
|
|
868
|
+
// Keep the unthemed source separate: live updates must not mistake our
|
|
869
|
+
// own previously applied colors for explicit caller customization.
|
|
870
|
+
const originalOptions = {
|
|
871
|
+
...baseOptions,
|
|
872
|
+
plugins: { ...baseOptions.plugins },
|
|
873
|
+
scales: baseOptions.scales ? { ...baseOptions.scales } : undefined,
|
|
874
|
+
};
|
|
875
|
+
chartTheme = createChartTheme(chartParams, originalOptions, themeRoot, chartConfig);
|
|
876
|
+
chartTheme.applyOptions(baseOptions);
|
|
877
|
+
}
|
|
878
|
+
|
|
643
879
|
// Create new chart
|
|
644
880
|
chartInstance = new Chart(canvasRef, chartConfig);
|
|
645
881
|
|
|
@@ -692,110 +928,139 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
692
928
|
<ContextAwareChartLayout
|
|
693
929
|
render={(isExpanded) => (
|
|
694
930
|
<div
|
|
695
|
-
|
|
931
|
+
ref={themeRoot}
|
|
932
|
+
class={`mcp-ui-root relative w-full bg-[color:var(--mcp-ui-surface,var(--color-white,#fff))] dark:bg-[color:var(--mcp-ui-surface,var(--color-gray-800,#1f2937))] rounded-lg shadow-[var(--mcp-ui-shadow,var(--shadow-sm))] border border-[color:var(--mcp-ui-border,var(--color-gray-200,#e5e7eb))] dark:border-[color:var(--mcp-ui-border,var(--color-gray-700,#374151))] overflow-hidden p-4 group ${
|
|
696
933
|
isExpanded() ? 'flex-1 min-h-0 flex flex-col' : ''
|
|
697
934
|
} ${params().className || ''}`}
|
|
698
935
|
>
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
class="
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
936
|
+
<div class="flex items-center justify-between gap-3 mb-3 flex-shrink-0">
|
|
937
|
+
<div class="min-w-0">
|
|
938
|
+
<Show when={params().title}>
|
|
939
|
+
<h3 class="text-sm font-semibold text-[color:var(--mcp-ui-fg,var(--color-gray-900,#111827))] dark:text-[color:var(--mcp-ui-fg,var(--color-white,#fff))]">
|
|
940
|
+
{params().title}
|
|
941
|
+
</h3>
|
|
942
|
+
</Show>
|
|
943
|
+
</div>
|
|
944
|
+
<div class="flex items-center gap-2">
|
|
945
|
+
<div
|
|
946
|
+
class="inline-flex rounded-md border border-[color:var(--mcp-ui-border,var(--color-gray-200,#e5e7eb))] p-0.5 dark:border-[color:var(--mcp-ui-border,var(--color-gray-600,#4b5563))]"
|
|
947
|
+
role="group"
|
|
948
|
+
aria-label={strings.chartViewSelector}
|
|
949
|
+
>
|
|
950
|
+
<button
|
|
951
|
+
type="button"
|
|
952
|
+
aria-pressed={activeView() === 'chart'}
|
|
953
|
+
class={`rounded px-2 py-1 text-xs font-medium transition-colors ${
|
|
954
|
+
activeView() === 'chart'
|
|
955
|
+
? 'bg-[color:var(--mcp-ui-accent,var(--color-gray-900,#111827))] text-[color:var(--mcp-ui-accent-fg,var(--color-white,#fff))] dark:bg-[color:var(--mcp-ui-accent,var(--color-gray-100,#f3f4f6))] dark:text-[color:var(--mcp-ui-accent-fg,var(--color-gray-900,#111827))]'
|
|
956
|
+
: 'text-[color:var(--mcp-ui-fg-muted,var(--color-gray-600,#4b5563))] hover:bg-[color:var(--mcp-ui-surface-hover,var(--color-gray-100,#f3f4f6))] dark:text-[color:var(--mcp-ui-fg,var(--color-gray-300,#d1d5db))] dark:hover:bg-[color:var(--mcp-ui-surface-hover,var(--color-gray-700,#374151))]'
|
|
957
|
+
}`}
|
|
958
|
+
onClick={() => setActiveView('chart')}
|
|
959
|
+
>
|
|
960
|
+
{strings.chartView}
|
|
961
|
+
</button>
|
|
962
|
+
<button
|
|
963
|
+
type="button"
|
|
964
|
+
aria-pressed={activeView() === 'data'}
|
|
965
|
+
class={`rounded px-2 py-1 text-xs font-medium transition-colors ${
|
|
966
|
+
activeView() === 'data'
|
|
967
|
+
? 'bg-[color:var(--mcp-ui-accent,var(--color-gray-900,#111827))] text-[color:var(--mcp-ui-accent-fg,var(--color-white,#fff))] dark:bg-[color:var(--mcp-ui-accent,var(--color-gray-100,#f3f4f6))] dark:text-[color:var(--mcp-ui-accent-fg,var(--color-gray-900,#111827))]'
|
|
968
|
+
: 'text-[color:var(--mcp-ui-fg-muted,var(--color-gray-600,#4b5563))] hover:bg-[color:var(--mcp-ui-surface-hover,var(--color-gray-100,#f3f4f6))] dark:text-[color:var(--mcp-ui-fg,var(--color-gray-300,#d1d5db))] dark:hover:bg-[color:var(--mcp-ui-surface-hover,var(--color-gray-700,#374151))]'
|
|
969
|
+
}`}
|
|
970
|
+
onClick={() => setActiveView('data')}
|
|
971
|
+
>
|
|
972
|
+
{strings.chartDataView}
|
|
973
|
+
</button>
|
|
974
|
+
</div>
|
|
975
|
+
{/* Zoom controls — permanently visible (not hover-revealed like
|
|
737
976
|
the export button) for as long as this chart is zoomable at
|
|
738
977
|
all. Zoom in/out come FIRST and unconditionally: they are the
|
|
739
978
|
only keyboard route into a feature whose gestures are all
|
|
740
979
|
pointer gestures, on a canvas that takes no focus. Reset joins
|
|
741
980
|
them only once the chart is actually off its initial viewport,
|
|
742
981
|
so the toolbar stays quiet on a chart nobody moved. */}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
982
|
+
<Show when={zoomControlsShown()}>
|
|
983
|
+
<div class="inline-flex items-center gap-1">
|
|
984
|
+
<button
|
|
985
|
+
type="button"
|
|
986
|
+
onClick={() => handleZoomBy(1 / CHART_ZOOM_BUTTON_FACTOR)}
|
|
987
|
+
class="opacity-60 hover:opacity-100 px-2 py-1 bg-[color:var(--mcp-ui-surface,var(--color-white,#fff))] dark:bg-[color:var(--mcp-ui-surface,var(--color-gray-800,#1f2937))] border border-[color:var(--mcp-ui-border,var(--color-gray-200,#e5e7eb))] dark:border-[color:var(--mcp-ui-border,var(--color-gray-600,#4b5563))] rounded-full hover:bg-[color:var(--mcp-ui-surface-hover,var(--color-gray-100,#f3f4f6))] dark:hover:bg-[color:var(--mcp-ui-surface-hover,var(--color-gray-700,#374151))] transition-all shadow-[var(--mcp-ui-shadow,var(--shadow-sm))]"
|
|
988
|
+
title={strings.zoomOut}
|
|
989
|
+
aria-label={strings.zoomOut}
|
|
990
|
+
>
|
|
991
|
+
<svg
|
|
992
|
+
class="w-3 h-3 text-[color:var(--mcp-ui-fg-muted,var(--color-gray-500,#6b7280))] dark:text-[color:var(--mcp-ui-fg-subtle,var(--color-gray-400,#9ca3af))]"
|
|
993
|
+
fill="none"
|
|
994
|
+
viewBox="0 0 24 24"
|
|
995
|
+
stroke="currentColor"
|
|
996
|
+
>
|
|
997
|
+
<path
|
|
998
|
+
stroke-linecap="round"
|
|
999
|
+
stroke-linejoin="round"
|
|
1000
|
+
stroke-width="2"
|
|
1001
|
+
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607zM13.5 10.5h-6"
|
|
1002
|
+
/>
|
|
1003
|
+
</svg>
|
|
1004
|
+
</button>
|
|
1005
|
+
|
|
1006
|
+
<button
|
|
1007
|
+
type="button"
|
|
1008
|
+
onClick={() => handleZoomBy(CHART_ZOOM_BUTTON_FACTOR)}
|
|
1009
|
+
class="opacity-60 hover:opacity-100 px-2 py-1 bg-[color:var(--mcp-ui-surface,var(--color-white,#fff))] dark:bg-[color:var(--mcp-ui-surface,var(--color-gray-800,#1f2937))] border border-[color:var(--mcp-ui-border,var(--color-gray-200,#e5e7eb))] dark:border-[color:var(--mcp-ui-border,var(--color-gray-600,#4b5563))] rounded-full hover:bg-[color:var(--mcp-ui-surface-hover,var(--color-gray-100,#f3f4f6))] dark:hover:bg-[color:var(--mcp-ui-surface-hover,var(--color-gray-700,#374151))] transition-all shadow-[var(--mcp-ui-shadow,var(--shadow-sm))]"
|
|
1010
|
+
title={strings.zoomIn}
|
|
1011
|
+
aria-label={strings.zoomIn}
|
|
1012
|
+
>
|
|
1013
|
+
<svg
|
|
1014
|
+
class="w-3 h-3 text-[color:var(--mcp-ui-fg-muted,var(--color-gray-500,#6b7280))] dark:text-[color:var(--mcp-ui-fg-subtle,var(--color-gray-400,#9ca3af))]"
|
|
1015
|
+
fill="none"
|
|
1016
|
+
viewBox="0 0 24 24"
|
|
1017
|
+
stroke="currentColor"
|
|
1018
|
+
>
|
|
1019
|
+
<path
|
|
1020
|
+
stroke-linecap="round"
|
|
1021
|
+
stroke-linejoin="round"
|
|
1022
|
+
stroke-width="2"
|
|
1023
|
+
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607zM10.5 7.5v6m3-3h-6"
|
|
1024
|
+
/>
|
|
1025
|
+
</svg>
|
|
1026
|
+
</button>
|
|
1027
|
+
|
|
1028
|
+
<Show when={isZoomed()}>
|
|
1029
|
+
<button
|
|
1030
|
+
type="button"
|
|
1031
|
+
onClick={handleResetZoom}
|
|
1032
|
+
class="opacity-60 hover:opacity-100 px-2 py-1 bg-[color:var(--mcp-ui-surface,var(--color-white,#fff))] dark:bg-[color:var(--mcp-ui-surface,var(--color-gray-800,#1f2937))] border border-[color:var(--mcp-ui-border,var(--color-gray-200,#e5e7eb))] dark:border-[color:var(--mcp-ui-border,var(--color-gray-600,#4b5563))] rounded-full hover:bg-[color:var(--mcp-ui-surface-hover,var(--color-gray-100,#f3f4f6))] dark:hover:bg-[color:var(--mcp-ui-surface-hover,var(--color-gray-700,#374151))] transition-all shadow-[var(--mcp-ui-shadow,var(--shadow-sm))]"
|
|
1033
|
+
title={strings.zoomReset}
|
|
1034
|
+
aria-label={strings.zoomReset}
|
|
1035
|
+
>
|
|
1036
|
+
<svg
|
|
1037
|
+
class="w-3 h-3 text-[color:var(--mcp-ui-fg-muted,var(--color-gray-500,#6b7280))] dark:text-[color:var(--mcp-ui-fg-subtle,var(--color-gray-400,#9ca3af))]"
|
|
1038
|
+
fill="none"
|
|
1039
|
+
viewBox="0 0 24 24"
|
|
1040
|
+
stroke="currentColor"
|
|
1041
|
+
>
|
|
1042
|
+
<path
|
|
1043
|
+
stroke-linecap="round"
|
|
1044
|
+
stroke-linejoin="round"
|
|
1045
|
+
stroke-width="2"
|
|
1046
|
+
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
1047
|
+
/>
|
|
1048
|
+
</svg>
|
|
1049
|
+
</button>
|
|
1050
|
+
</Show>
|
|
1051
|
+
</div>
|
|
1052
|
+
</Show>
|
|
1053
|
+
<Show when={exportEnabled()}>
|
|
790
1054
|
<button
|
|
791
1055
|
type="button"
|
|
792
|
-
onClick={
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1056
|
+
onClick={handleExportPNG}
|
|
1057
|
+
disabled={activeView() !== 'chart' || isLoading() || Boolean(error())}
|
|
1058
|
+
class="opacity-0 group-hover:opacity-60 hover:!opacity-100 disabled:!opacity-30 disabled:cursor-not-allowed px-2 py-1 bg-[color:var(--mcp-ui-surface,var(--color-white,#fff))] dark:bg-[color:var(--mcp-ui-surface,var(--color-gray-800,#1f2937))] border border-[color:var(--mcp-ui-border,var(--color-gray-200,#e5e7eb))] dark:border-[color:var(--mcp-ui-border,var(--color-gray-600,#4b5563))] rounded-full hover:bg-[color:var(--mcp-ui-surface-hover,var(--color-gray-100,#f3f4f6))] dark:hover:bg-[color:var(--mcp-ui-surface-hover,var(--color-gray-700,#374151))] transition-all shadow-[var(--mcp-ui-shadow,var(--shadow-sm))]"
|
|
1059
|
+
title={strings.chartDownloadPng}
|
|
1060
|
+
aria-label={strings.chartDownloadPngAria}
|
|
796
1061
|
>
|
|
797
1062
|
<svg
|
|
798
|
-
class="w-3 h-3 text-gray-500 dark:text-gray-400"
|
|
1063
|
+
class="w-3 h-3 text-[color:var(--mcp-ui-fg-muted,var(--color-gray-500,#6b7280))] dark:text-[color:var(--mcp-ui-fg-subtle,var(--color-gray-400,#9ca3af))]"
|
|
799
1064
|
fill="none"
|
|
800
1065
|
viewBox="0 0 24 24"
|
|
801
1066
|
stroke="currentColor"
|
|
@@ -804,170 +1069,149 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
804
1069
|
stroke-linecap="round"
|
|
805
1070
|
stroke-linejoin="round"
|
|
806
1071
|
stroke-width="2"
|
|
807
|
-
d="
|
|
1072
|
+
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
|
808
1073
|
/>
|
|
809
1074
|
</svg>
|
|
810
1075
|
</button>
|
|
811
1076
|
</Show>
|
|
812
1077
|
</div>
|
|
813
|
-
</
|
|
814
|
-
<Show when={exportEnabled()}>
|
|
815
|
-
<button
|
|
816
|
-
type="button"
|
|
817
|
-
onClick={handleExportPNG}
|
|
818
|
-
disabled={activeView() !== 'chart' || isLoading() || Boolean(error())}
|
|
819
|
-
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"
|
|
820
|
-
title={strings.chartDownloadPng}
|
|
821
|
-
aria-label={strings.chartDownloadPngAria}
|
|
822
|
-
>
|
|
823
|
-
<svg
|
|
824
|
-
class="w-3 h-3 text-gray-500 dark:text-gray-400"
|
|
825
|
-
fill="none"
|
|
826
|
-
viewBox="0 0 24 24"
|
|
827
|
-
stroke="currentColor"
|
|
828
|
-
>
|
|
829
|
-
<path
|
|
830
|
-
stroke-linecap="round"
|
|
831
|
-
stroke-linejoin="round"
|
|
832
|
-
stroke-width="2"
|
|
833
|
-
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
|
834
|
-
/>
|
|
835
|
-
</svg>
|
|
836
|
-
</button>
|
|
837
|
-
</Show>
|
|
838
|
-
</div>
|
|
839
|
-
</div>
|
|
1078
|
+
</div>
|
|
840
1079
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1080
|
+
<p id={descriptionId} class="sr-only">
|
|
1081
|
+
{title()}. {strings.chartDataSummary}
|
|
1082
|
+
</p>
|
|
844
1083
|
|
|
845
|
-
|
|
1084
|
+
{/* Fallback ladder (P2.5): degrade to a series table on render error
|
|
846
1085
|
instead of a bare "Chart Error" message. */}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
<For each={tableData().columns}>
|
|
873
|
-
{(column) => (
|
|
874
|
-
<th scope="col" class="px-3 py-2 font-medium text-gray-900 dark:text-gray-100">
|
|
875
|
-
{column}
|
|
876
|
-
</th>
|
|
877
|
-
)}
|
|
878
|
-
</For>
|
|
879
|
-
</tr>
|
|
880
|
-
</thead>
|
|
881
|
-
<tbody>
|
|
882
|
-
<For each={tableData().rows}>
|
|
883
|
-
{(row, rowIndex) => (
|
|
884
|
-
<tr class="border-t border-gray-100 dark:border-gray-700">
|
|
1086
|
+
<Show when={error() && activeView() === 'chart'}>
|
|
1087
|
+
<DegradedFallback
|
|
1088
|
+
message={formatMCPUIString(strings.chartRenderFailed, { error: String(error()) })}
|
|
1089
|
+
caption={strings.chartDegradedCaption}
|
|
1090
|
+
{...tableData()}
|
|
1091
|
+
/>
|
|
1092
|
+
</Show>
|
|
1093
|
+
|
|
1094
|
+
<Show when={activeView() === 'data'}>
|
|
1095
|
+
<div
|
|
1096
|
+
class={`w-full overflow-auto rounded border border-[color:var(--mcp-ui-border,var(--color-gray-200,#e5e7eb))] dark:border-[color:var(--mcp-ui-border,var(--color-gray-700,#374151))] ${
|
|
1097
|
+
isExpanded() ? 'flex-1 min-h-0' : ''
|
|
1098
|
+
}`}
|
|
1099
|
+
style={
|
|
1100
|
+
isExpanded()
|
|
1101
|
+
? undefined
|
|
1102
|
+
: { height: params().height || '250px', 'max-height': '70vh' }
|
|
1103
|
+
}
|
|
1104
|
+
>
|
|
1105
|
+
<table class="w-full border-collapse text-left text-sm">
|
|
1106
|
+
<caption class="sr-only">
|
|
1107
|
+
{title()} — {strings.chartDataTable}
|
|
1108
|
+
</caption>
|
|
1109
|
+
<thead class="bg-[color:var(--mcp-ui-surface-muted,var(--color-gray-50,#f9fafb))] dark:bg-[color:var(--mcp-ui-surface,color-mix(in_oklab,var(--color-gray-900,#111827)_40%,transparent))]">
|
|
1110
|
+
<tr>
|
|
885
1111
|
<For each={tableData().columns}>
|
|
886
|
-
{(
|
|
887
|
-
<
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
</Show>
|
|
894
|
-
</td>
|
|
1112
|
+
{(column) => (
|
|
1113
|
+
<th
|
|
1114
|
+
scope="col"
|
|
1115
|
+
class="px-3 py-2 font-medium text-[color:var(--mcp-ui-fg,var(--color-gray-900,#111827))] dark:text-[color:var(--mcp-ui-fg,var(--color-gray-100,#f3f4f6))]"
|
|
1116
|
+
>
|
|
1117
|
+
{column}
|
|
1118
|
+
</th>
|
|
895
1119
|
)}
|
|
896
1120
|
</For>
|
|
897
1121
|
</tr>
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
role="img"
|
|
931
|
-
aria-label={title()}
|
|
932
|
-
aria-describedby={notesShown() ? `${descriptionId} ${notesId}` : descriptionId}
|
|
933
|
-
aria-hidden={activeView() !== 'chart' || Boolean(error())}
|
|
934
|
-
/>
|
|
935
|
-
<Show when={isLoading() && activeView() === 'chart'}>
|
|
936
|
-
<div class="absolute inset-0 flex items-center justify-center bg-white/80 dark:bg-gray-800/80">
|
|
937
|
-
<div class="flex flex-col items-center gap-2">
|
|
938
|
-
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
|
|
939
|
-
<span class="text-sm text-gray-500 dark:text-gray-400">{strings.chartLoading}</span>
|
|
1122
|
+
</thead>
|
|
1123
|
+
<tbody>
|
|
1124
|
+
<For each={tableData().rows}>
|
|
1125
|
+
{(row, rowIndex) => (
|
|
1126
|
+
<tr class="border-t border-[color:var(--mcp-ui-border,var(--color-gray-100,#f3f4f6))] dark:border-[color:var(--mcp-ui-border,var(--color-gray-700,#374151))]">
|
|
1127
|
+
<For each={tableData().columns}>
|
|
1128
|
+
{(_column, index) => (
|
|
1129
|
+
<td class="px-3 py-2 text-[color:var(--mcp-ui-fg,var(--color-gray-700,#374151))] dark:text-[color:var(--mcp-ui-fg,var(--color-gray-300,#d1d5db))]">
|
|
1130
|
+
<Show
|
|
1131
|
+
when={tableData().missing[rowIndex()]?.[index()]}
|
|
1132
|
+
fallback={String(row[index()] ?? '')}
|
|
1133
|
+
>
|
|
1134
|
+
<MissingValueMark />
|
|
1135
|
+
</Show>
|
|
1136
|
+
</td>
|
|
1137
|
+
)}
|
|
1138
|
+
</For>
|
|
1139
|
+
</tr>
|
|
1140
|
+
)}
|
|
1141
|
+
</For>
|
|
1142
|
+
<Show when={tableData().rows.length === 0}>
|
|
1143
|
+
<tr>
|
|
1144
|
+
<td
|
|
1145
|
+
colSpan={Math.max(1, tableData().columns.length)}
|
|
1146
|
+
class="px-3 py-6 text-center text-[color:var(--mcp-ui-fg-muted,var(--color-gray-500,#6b7280))] dark:text-[color:var(--mcp-ui-fg-subtle,var(--color-gray-400,#9ca3af))]"
|
|
1147
|
+
>
|
|
1148
|
+
{strings.chartNoData}
|
|
1149
|
+
</td>
|
|
1150
|
+
</tr>
|
|
1151
|
+
</Show>
|
|
1152
|
+
</tbody>
|
|
1153
|
+
</table>
|
|
940
1154
|
</div>
|
|
1155
|
+
<Show when={anyMissingCell()}>
|
|
1156
|
+
<MissingValueLegend class="mt-2" />
|
|
1157
|
+
</Show>
|
|
1158
|
+
</Show>
|
|
1159
|
+
|
|
1160
|
+
<div
|
|
1161
|
+
class={`relative w-full ${isExpanded() ? 'flex-1 min-h-0' : ''}`}
|
|
1162
|
+
style={
|
|
1163
|
+
error() || activeView() === 'data'
|
|
1164
|
+
? { display: 'none' }
|
|
1165
|
+
: isExpanded()
|
|
1166
|
+
? { height: '100%', display: 'block' }
|
|
1167
|
+
: { height: params().height || '250px', display: 'block' }
|
|
1168
|
+
}
|
|
1169
|
+
>
|
|
1170
|
+
<canvas
|
|
1171
|
+
ref={canvasRef}
|
|
1172
|
+
role="img"
|
|
1173
|
+
aria-label={title()}
|
|
1174
|
+
aria-describedby={notesShown() ? `${descriptionId} ${notesId}` : descriptionId}
|
|
1175
|
+
aria-hidden={activeView() !== 'chart' || Boolean(error())}
|
|
1176
|
+
/>
|
|
1177
|
+
<Show when={isLoading() && activeView() === 'chart'}>
|
|
1178
|
+
<div class="absolute inset-0 flex items-center justify-center bg-[color:var(--mcp-ui-surface,color-mix(in_oklab,var(--color-white,#fff)_80%,transparent))] dark:bg-[color:var(--mcp-ui-surface,color-mix(in_oklab,var(--color-gray-800,#1f2937)_80%,transparent))]">
|
|
1179
|
+
<div class="flex flex-col items-center gap-2">
|
|
1180
|
+
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-[color:var(--mcp-ui-accent-border,var(--color-blue-600))]" />
|
|
1181
|
+
<span class="text-sm text-[color:var(--mcp-ui-fg-muted,var(--color-gray-500,#6b7280))] dark:text-[color:var(--mcp-ui-fg-subtle,var(--color-gray-400,#9ca3af))]">
|
|
1182
|
+
{strings.chartLoading}
|
|
1183
|
+
</span>
|
|
1184
|
+
</div>
|
|
1185
|
+
</div>
|
|
1186
|
+
</Show>
|
|
941
1187
|
</div>
|
|
942
|
-
</Show>
|
|
943
|
-
</div>
|
|
944
1188
|
|
|
945
|
-
|
|
1189
|
+
{/* Missing values (v6.24.0): how THIS chart type shows them, and which
|
|
946
1190
|
series have no data at all. Linked from the canvas, so a screen
|
|
947
1191
|
reader hears the convention with the chart. */}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1192
|
+
<Show when={notesShown()}>
|
|
1193
|
+
<div
|
|
1194
|
+
data-mcp-chart-notes=""
|
|
1195
|
+
id={notesId}
|
|
1196
|
+
class="mt-2 space-y-0.5 text-xs text-[color:var(--mcp-ui-fg-muted,var(--color-gray-600,#4b5563))] dark:text-[color:var(--mcp-ui-fg-subtle,var(--color-gray-400,#9ca3af))]"
|
|
1197
|
+
>
|
|
1198
|
+
<Show when={conventionNoteShown()}>
|
|
1199
|
+
<p data-mcp-chart-note={noteKind()}>
|
|
1200
|
+
{noteKind() === 'bars'
|
|
1201
|
+
? strings.chartMissingBars
|
|
1202
|
+
: noteKind() === 'omitted'
|
|
1203
|
+
? strings.chartMissingPoints
|
|
1204
|
+
: strings.chartMissingGaps}
|
|
1205
|
+
</p>
|
|
1206
|
+
</Show>
|
|
1207
|
+
<Show when={missingSummary().fullyMissing.some(Boolean)}>
|
|
1208
|
+
<p data-mcp-chart-note="no-data">
|
|
1209
|
+
{formatMCPUIString(strings.chartSeriesNoData, { series: noDataSeries() })}
|
|
1210
|
+
</p>
|
|
1211
|
+
</Show>
|
|
1212
|
+
</div>
|
|
967
1213
|
</Show>
|
|
968
1214
|
</div>
|
|
969
|
-
</Show>
|
|
970
|
-
</div>
|
|
971
1215
|
)}
|
|
972
1216
|
/>
|
|
973
1217
|
</ExpandableWrapper>
|