@seed-ship/mcp-ui-solid 6.23.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.
Files changed (172) hide show
  1. package/CHANGELOG.md +167 -0
  2. package/README.md +154 -2
  3. package/dist/components/ActionGroupRenderer.cjs +6 -6
  4. package/dist/components/ActionGroupRenderer.cjs.map +1 -1
  5. package/dist/components/ActionGroupRenderer.js +6 -6
  6. package/dist/components/ActionGroupRenderer.js.map +1 -1
  7. package/dist/components/AutocompleteDropdown.cjs +6 -6
  8. package/dist/components/AutocompleteDropdown.cjs.map +1 -1
  9. package/dist/components/AutocompleteDropdown.js +6 -6
  10. package/dist/components/AutocompleteDropdown.js.map +1 -1
  11. package/dist/components/AutocompleteFormField.cjs +6 -5
  12. package/dist/components/AutocompleteFormField.cjs.map +1 -1
  13. package/dist/components/AutocompleteFormField.d.ts.map +1 -1
  14. package/dist/components/AutocompleteFormField.js +6 -5
  15. package/dist/components/AutocompleteFormField.js.map +1 -1
  16. package/dist/components/ChartJSRenderer.cjs +406 -70
  17. package/dist/components/ChartJSRenderer.cjs.map +1 -1
  18. package/dist/components/ChartJSRenderer.d.ts.map +1 -1
  19. package/dist/components/ChartJSRenderer.js +407 -71
  20. package/dist/components/ChartJSRenderer.js.map +1 -1
  21. package/dist/components/ChatPrompt.cjs +5 -5
  22. package/dist/components/ChatPrompt.cjs.map +1 -1
  23. package/dist/components/ChatPrompt.js +5 -5
  24. package/dist/components/ChatPrompt.js.map +1 -1
  25. package/dist/components/ComponentToolbar.cjs +5 -5
  26. package/dist/components/ComponentToolbar.cjs.map +1 -1
  27. package/dist/components/ComponentToolbar.js +5 -5
  28. package/dist/components/ComponentToolbar.js.map +1 -1
  29. package/dist/components/DegradedFallback.cjs +35 -3
  30. package/dist/components/DegradedFallback.cjs.map +1 -1
  31. package/dist/components/DegradedFallback.d.ts +7 -0
  32. package/dist/components/DegradedFallback.d.ts.map +1 -1
  33. package/dist/components/DegradedFallback.js +35 -3
  34. package/dist/components/DegradedFallback.js.map +1 -1
  35. package/dist/components/ExpandableWrapper.cjs +10 -7
  36. package/dist/components/ExpandableWrapper.cjs.map +1 -1
  37. package/dist/components/ExpandableWrapper.d.ts.map +1 -1
  38. package/dist/components/ExpandableWrapper.js +10 -7
  39. package/dist/components/ExpandableWrapper.js.map +1 -1
  40. package/dist/components/FormFieldRenderer.cjs +13 -12
  41. package/dist/components/FormFieldRenderer.cjs.map +1 -1
  42. package/dist/components/FormFieldRenderer.d.ts.map +1 -1
  43. package/dist/components/FormFieldRenderer.js +13 -12
  44. package/dist/components/FormFieldRenderer.js.map +1 -1
  45. package/dist/components/FormRenderer.cjs +1 -1
  46. package/dist/components/FormRenderer.cjs.map +1 -1
  47. package/dist/components/FormRenderer.js +1 -1
  48. package/dist/components/FormRenderer.js.map +1 -1
  49. package/dist/components/GenerativeUIErrorBoundary.cjs +1 -1
  50. package/dist/components/GenerativeUIErrorBoundary.cjs.map +1 -1
  51. package/dist/components/GenerativeUIErrorBoundary.js +1 -1
  52. package/dist/components/GenerativeUIErrorBoundary.js.map +1 -1
  53. package/dist/components/GhostText.cjs +2 -2
  54. package/dist/components/GhostText.cjs.map +1 -1
  55. package/dist/components/GhostText.js +2 -2
  56. package/dist/components/GhostText.js.map +1 -1
  57. package/dist/components/GraphRenderer.cjs +100 -29
  58. package/dist/components/GraphRenderer.cjs.map +1 -1
  59. package/dist/components/GraphRenderer.d.ts +1 -1
  60. package/dist/components/GraphRenderer.d.ts.map +1 -1
  61. package/dist/components/GraphRenderer.js +100 -29
  62. package/dist/components/GraphRenderer.js.map +1 -1
  63. package/dist/components/MissingValue.cjs +43 -0
  64. package/dist/components/MissingValue.cjs.map +1 -0
  65. package/dist/components/MissingValue.d.ts +27 -0
  66. package/dist/components/MissingValue.d.ts.map +1 -0
  67. package/dist/components/MissingValue.js +43 -0
  68. package/dist/components/MissingValue.js.map +1 -0
  69. package/dist/components/ModalRenderer.cjs +39 -31
  70. package/dist/components/ModalRenderer.cjs.map +1 -1
  71. package/dist/components/ModalRenderer.d.ts.map +1 -1
  72. package/dist/components/ModalRenderer.js +40 -32
  73. package/dist/components/ModalRenderer.js.map +1 -1
  74. package/dist/components/PortalDropdownMenu.cjs +6 -3
  75. package/dist/components/PortalDropdownMenu.cjs.map +1 -1
  76. package/dist/components/PortalDropdownMenu.d.ts.map +1 -1
  77. package/dist/components/PortalDropdownMenu.js +7 -4
  78. package/dist/components/PortalDropdownMenu.js.map +1 -1
  79. package/dist/components/ScratchpadPanel.cjs +38 -44
  80. package/dist/components/ScratchpadPanel.cjs.map +1 -1
  81. package/dist/components/ScratchpadPanel.d.ts.map +1 -1
  82. package/dist/components/ScratchpadPanel.js +38 -44
  83. package/dist/components/ScratchpadPanel.js.map +1 -1
  84. package/dist/components/UIResourceRenderer.cjs +255 -217
  85. package/dist/components/UIResourceRenderer.cjs.map +1 -1
  86. package/dist/components/UIResourceRenderer.d.ts.map +1 -1
  87. package/dist/components/UIResourceRenderer.js +255 -217
  88. package/dist/components/UIResourceRenderer.js.map +1 -1
  89. package/dist/components/chart-data-table.cjs +30 -5
  90. package/dist/components/chart-data-table.cjs.map +1 -1
  91. package/dist/components/chart-data-table.d.ts +18 -1
  92. package/dist/components/chart-data-table.d.ts.map +1 -1
  93. package/dist/components/chart-data-table.js +30 -5
  94. package/dist/components/chart-data-table.js.map +1 -1
  95. package/dist/components/chart-missing-plugin.cjs +194 -0
  96. package/dist/components/chart-missing-plugin.cjs.map +1 -0
  97. package/dist/components/chart-missing-plugin.d.ts +129 -0
  98. package/dist/components/chart-missing-plugin.d.ts.map +1 -0
  99. package/dist/components/chart-missing-plugin.js +194 -0
  100. package/dist/components/chart-missing-plugin.js.map +1 -0
  101. package/dist/context/MCPUIStringsContext.cjs +12 -1
  102. package/dist/context/MCPUIStringsContext.cjs.map +1 -1
  103. package/dist/context/MCPUIStringsContext.d.ts +62 -0
  104. package/dist/context/MCPUIStringsContext.d.ts.map +1 -1
  105. package/dist/context/MCPUIStringsContext.js +12 -1
  106. package/dist/context/MCPUIStringsContext.js.map +1 -1
  107. package/dist/services/component-registry.cjs +9 -2
  108. package/dist/services/component-registry.cjs.map +1 -1
  109. package/dist/services/component-registry.d.ts.map +1 -1
  110. package/dist/services/component-registry.js +9 -2
  111. package/dist/services/component-registry.js.map +1 -1
  112. package/dist/services/validation.cjs +31 -8
  113. package/dist/services/validation.cjs.map +1 -1
  114. package/dist/services/validation.d.ts.map +1 -1
  115. package/dist/services/validation.js +31 -8
  116. package/dist/services/validation.js.map +1 -1
  117. package/dist/types/index.d.ts +14 -2
  118. package/dist/types/index.d.ts.map +1 -1
  119. package/dist/utils/degraded-projections.cjs +19 -3
  120. package/dist/utils/degraded-projections.cjs.map +1 -1
  121. package/dist/utils/degraded-projections.d.ts +22 -2
  122. package/dist/utils/degraded-projections.d.ts.map +1 -1
  123. package/dist/utils/degraded-projections.js +19 -3
  124. package/dist/utils/degraded-projections.js.map +1 -1
  125. package/dist/utils/math-markdown.cjs +1 -1
  126. package/dist/utils/math-markdown.cjs.map +1 -1
  127. package/dist/utils/math-markdown.js +1 -1
  128. package/dist/utils/math-markdown.js.map +1 -1
  129. package/dist/utils/missing-value.cjs +60 -0
  130. package/dist/utils/missing-value.cjs.map +1 -0
  131. package/dist/utils/missing-value.d.ts +79 -0
  132. package/dist/utils/missing-value.d.ts.map +1 -0
  133. package/dist/utils/missing-value.js +60 -0
  134. package/dist/utils/missing-value.js.map +1 -0
  135. package/dist/utils/theme.cjs +91 -0
  136. package/dist/utils/theme.cjs.map +1 -0
  137. package/dist/utils/theme.d.ts +11 -0
  138. package/dist/utils/theme.d.ts.map +1 -0
  139. package/dist/utils/theme.js +91 -0
  140. package/dist/utils/theme.js.map +1 -0
  141. package/docs/SECURITY-DEPENDENCIES.md +81 -0
  142. package/docs/THEMING.md +205 -0
  143. package/package.json +7 -5
  144. package/src/components/ActionGroupRenderer.tsx +6 -6
  145. package/src/components/AutocompleteDropdown.tsx +17 -17
  146. package/src/components/AutocompleteFormField.tsx +10 -9
  147. package/src/components/ChartJSRenderer.tsx +609 -212
  148. package/src/components/ChatPrompt.tsx +16 -16
  149. package/src/components/ComponentToolbar.tsx +5 -5
  150. package/src/components/DegradedFallback.tsx +33 -11
  151. package/src/components/ExpandableWrapper.tsx +15 -11
  152. package/src/components/FormFieldRenderer.tsx +52 -51
  153. package/src/components/FormRenderer.tsx +12 -12
  154. package/src/components/GenerativeUIErrorBoundary.tsx +6 -6
  155. package/src/components/GhostText.tsx +7 -7
  156. package/src/components/GraphRenderer.tsx +83 -67
  157. package/src/components/MissingValue.tsx +61 -0
  158. package/src/components/ModalRenderer.tsx +11 -5
  159. package/src/components/PortalDropdownMenu.tsx +6 -2
  160. package/src/components/ScratchpadPanel.tsx +144 -141
  161. package/src/components/UIResourceRenderer.tsx +143 -110
  162. package/src/components/chart-data-table.ts +50 -6
  163. package/src/components/chart-missing-plugin.ts +336 -0
  164. package/src/context/MCPUIStringsContext.tsx +76 -0
  165. package/src/services/component-registry.ts +10 -2
  166. package/src/services/validation.ts +44 -9
  167. package/src/styles/autocomplete.css +53 -53
  168. package/src/types/index.ts +13 -1
  169. package/src/utils/degraded-projections.ts +44 -4
  170. package/src/utils/math-markdown.ts +1 -1
  171. package/src/utils/missing-value.ts +127 -0
  172. package/src/utils/theme.ts +101 -0
@@ -20,10 +20,7 @@
20
20
 
21
21
  import { Component, createSignal, onCleanup, onMount, Show, For } from 'solid-js';
22
22
  import type { UIComponent } from '../types';
23
- import type {
24
- GraphComponentParams,
25
- GraphLayout,
26
- } from '@seed-ship/mcp-ui-spec';
23
+ import type { GraphComponentParams, GraphLayout } from '@seed-ship/mcp-ui-spec';
27
24
  import { ExpandableWrapper, useExpanded } from './ExpandableWrapper';
28
25
  import { PortalDropdownMenu } from './PortalDropdownMenu';
29
26
  import { DegradedFallback } from './DegradedFallback';
@@ -31,6 +28,40 @@ import { graphToDegradedTable } from '../utils/degraded-projections';
31
28
  import { escapeHtml } from '../utils/escape-html';
32
29
  import { useTelemetry } from '../context/MCPUITelemetryContext';
33
30
  import { formatMCPUIString, useMCPUIStrings } from '../context/MCPUIStringsContext';
31
+ import { observeTheme, readThemeTokens } from '../utils/theme';
32
+
33
+ const GRAPH_THEME_TOKENS = [
34
+ ...Array.from({ length: 8 }, (_, i) => `--mcp-ui-chart-${i + 1}`),
35
+ '--mcp-ui-chart-grid',
36
+ '--mcp-ui-chart-axis',
37
+ '--mcp-ui-chart-label',
38
+ ] as const;
39
+
40
+ /** G6's option mappers outrank datum.style, so explicitly retain the latter. */
41
+ function graphThemeOptions(p: GraphComponentParams, tokens: Record<string, string | undefined>) {
42
+ const nodeIndices = new Map(p.nodes.map((node, index) => [node.id, index]));
43
+ return {
44
+ node: {
45
+ style: (datum: any) => {
46
+ const index = nodeIndices.get(datum.id) ?? 0;
47
+ const fill = tokens[`--mcp-ui-chart-${(index % 8) + 1}`];
48
+ return {
49
+ ...(fill ? { fill } : {}),
50
+ ...(tokens['--mcp-ui-chart-axis'] ? { stroke: tokens['--mcp-ui-chart-axis'] } : {}),
51
+ ...(tokens['--mcp-ui-chart-label'] ? { labelFill: tokens['--mcp-ui-chart-label'] } : {}),
52
+ ...datum.style,
53
+ };
54
+ },
55
+ },
56
+ edge: {
57
+ style: (datum: any) => ({
58
+ ...(tokens['--mcp-ui-chart-grid'] ? { stroke: tokens['--mcp-ui-chart-grid'] } : {}),
59
+ ...(tokens['--mcp-ui-chart-label'] ? { labelFill: tokens['--mcp-ui-chart-label'] } : {}),
60
+ ...datum.style,
61
+ }),
62
+ },
63
+ };
64
+ }
34
65
 
35
66
  // Module-scoped lazy import promise — first call triggers the dynamic
36
67
  // import, subsequent calls reuse the resolved module.
@@ -94,7 +125,8 @@ function resolveLayout(params: GraphComponentParams): { type: string; [key: stri
94
125
  */
95
126
  export function buildGraphConfig(
96
127
  p: GraphComponentParams,
97
- container?: HTMLElement
128
+ container?: HTMLElement,
129
+ tokens: Record<string, string | undefined> = {}
98
130
  ): Record<string, unknown> {
99
131
  const config: Record<string, unknown> = {
100
132
  container,
@@ -102,6 +134,9 @@ export function buildGraphConfig(
102
134
  layout: resolveLayout(p),
103
135
  behaviors: resolveBehaviors(p),
104
136
  };
137
+ if (GRAPH_THEME_TOKENS.some((name) => tokens[name])) {
138
+ Object.assign(config, graphThemeOptions(p, tokens));
139
+ }
105
140
 
106
141
  if (p.rendererPref === 'svg') {
107
142
  console.warn(
@@ -122,7 +157,14 @@ export function buildGraphConfig(
122
157
  if (!item) return '';
123
158
  const label = item.label ?? item.id ?? '';
124
159
  const data = item.data ? JSON.stringify(item.data) : '';
125
- return `<div style="padding:4px 8px"><strong>${escapeHtml(String(label))}</strong>${
160
+ // G6 may position HTML outside our subtree. Resolve effective host
161
+ // values at hover time, rather than relying on portal inheritance.
162
+ const theme = readThemeTokens(container, ['--mcp-ui-surface', '--mcp-ui-fg', '--mcp-ui-border']);
163
+ const styles = ['padding:4px 8px'];
164
+ if (theme['--mcp-ui-surface']) styles.push(`background-color:${theme['--mcp-ui-surface']}`);
165
+ if (theme['--mcp-ui-fg']) styles.push(`color:${theme['--mcp-ui-fg']}`);
166
+ if (theme['--mcp-ui-border']) styles.push(`border:1px solid ${theme['--mcp-ui-border']}`);
167
+ return `<div style="${escapeHtml(styles.join(';'))}"><strong>${escapeHtml(String(label))}</strong>${
126
168
  data ? `<br><span style="font-size:11px;opacity:0.7">${escapeHtml(data)}</span>` : ''
127
169
  }</div>`;
128
170
  },
@@ -234,70 +276,40 @@ export const GraphRenderer: Component<GraphRendererProps> = (props) => {
234
276
  // Loosely typed because G6 is a peer-optional — we don't pull its
235
277
  // types into the bundle just to type a transient local handle.
236
278
  let graphInstance: any | undefined;
279
+ let stopTheme: (() => void) | undefined;
280
+ let disposed = false;
237
281
 
238
282
  onMount(async () => {
239
283
  const g6Available = await isG6Available();
284
+ if (disposed) return;
240
285
  setAvailable(g6Available);
241
286
  if (!g6Available || !containerRef) return;
242
287
 
243
288
  try {
244
289
  const { Graph } = await g6ModulePromise!;
290
+ if (disposed || !containerRef) return;
245
291
  const p = params();
246
- const config: Record<string, unknown> = {
247
- container: containerRef,
248
- data: { nodes: p.nodes, edges: p.edges ?? [] },
249
- layout: resolveLayout(p),
250
- behaviors: resolveBehaviors(p),
251
- };
292
+ let tokens = readThemeTokens(containerRef, GRAPH_THEME_TOKENS);
293
+ const config = buildGraphConfig(p, containerRef, tokens);
252
294
 
253
- // G6 v5's `renderer` is a factory `(layer) => IRenderer`, NOT a string
254
- // (that was the v4 contract). Passing the string `'canvas'` / `'svg'`
255
- // makes G6 throw `renderer is not a function` — and because the default
256
- // path also passed the string `'canvas'`, EVERY graph crashed, not just
257
- // the svg one.
258
- //
259
- // Fix: omit `renderer` entirely. G6 then uses its built-in canvas
260
- // renderer (documented default `() => new CanvasRenderer()`), which is
261
- // what we want for every graph.
262
- //
263
- // `rendererPref: 'svg'` is NOT wired yet: a real G6 v5 SVG renderer
264
- // needs the `@antv/g-svg` factory, which is a *transitive* dep of
265
- // `@antv/g6` (not a declared peer) and isn't statically resolvable at
266
- // build time. Rather than ship a fragile/unresolvable import, we treat
267
- // svg as "not yet supported" and fall back to the canvas default with a
268
- // one-time warning. Tracked for a follow-up that wires the factory
269
- // behind a proper optional-peer resolution. (Never pass a string here.)
270
- if (p.rendererPref === 'svg') {
271
- console.warn(
272
- '[MCP-UI] GraphRenderer: rendererPref "svg" is not yet supported; using the default canvas renderer.'
273
- );
274
- }
275
- if (p.fitView !== false) {
276
- config.autoFit = 'view';
277
- }
278
- if (p.tooltip !== false) {
279
- // Built-in tooltip plugin — shows label + a compact dump of
280
- // node.data on hover. Consumers can opt out with `tooltip: false`.
281
- config.plugins = [
282
- {
283
- type: 'tooltip',
284
- getContent: (_evt: unknown, items: any[]) => {
285
- const item = items?.[0];
286
- if (!item) return '';
287
- const label = item.label ?? item.id ?? '';
288
- const data = item.data ? JSON.stringify(item.data) : '';
289
- return `<div style="padding:4px 8px"><strong>${escapeHtml(String(label))}</strong>${
290
- data
291
- ? `<br><span style="font-size:11px;opacity:0.7">${escapeHtml(data)}</span>`
292
- : ''
293
- }</div>`;
294
- },
295
- },
296
- ];
297
- }
298
295
  graphInstance = new (Graph as any)(config);
299
296
  await graphInstance.render();
297
+ if (disposed) return;
298
+ const syncTheme = () => {
299
+ if (!graphInstance || disposed) return;
300
+ const next = readThemeTokens(containerRef, GRAPH_THEME_TOKENS);
301
+ if (GRAPH_THEME_TOKENS.every((name) => next[name] === tokens[name])) return;
302
+ tokens = next;
303
+ graphInstance.setOptions(graphThemeOptions(p, tokens));
304
+ // draw() updates styles without rerunning force layout or auto-fit.
305
+ void Promise.resolve(graphInstance.draw()).catch(() => {});
306
+ };
307
+ stopTheme = observeTheme(containerRef, syncTheme);
308
+ // Rendering/layout is async: a host may have switched themes while it
309
+ // was in flight, before the observer could be attached.
310
+ syncTheme();
300
311
  } catch (err) {
312
+ if (disposed) return;
301
313
  const message = err instanceof Error ? err.message : strings.graphRenderError;
302
314
  setError(message);
303
315
  // Fallback ladder (P2.5): the native G6 render threw — emit telemetry
@@ -314,6 +326,8 @@ export const GraphRenderer: Component<GraphRendererProps> = (props) => {
314
326
  });
315
327
 
316
328
  onCleanup(() => {
329
+ disposed = true;
330
+ stopTheme?.();
317
331
  try {
318
332
  graphInstance?.destroy();
319
333
  } catch {
@@ -365,17 +379,17 @@ export const GraphRenderer: Component<GraphRendererProps> = (props) => {
365
379
  when={available() === false}
366
380
  fallback={
367
381
  // Loading skeleton while we determine peer availability
368
- <div class="w-full p-4 bg-gray-50 dark:bg-gray-800/40 border border-gray-200 dark:border-gray-700 rounded-lg animate-pulse">
369
- <div class="h-4 w-32 bg-gray-200 dark:bg-gray-700 rounded mb-2" />
370
- <div class="h-3 w-48 bg-gray-200 dark:bg-gray-700 rounded" />
382
+ <div class="w-full p-4 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-800,#1f2937)_40%,transparent))] border border-[color:var(--mcp-ui-border,var(--color-gray-200,#e5e7eb))] dark:border-[color:var(--mcp-ui-border,var(--color-gray-700,#374151))] rounded-lg animate-pulse">
383
+ <div class="h-4 w-32 bg-[color:var(--mcp-ui-surface-muted,var(--color-gray-200,#e5e7eb))] dark:bg-[color:var(--mcp-ui-surface-muted,var(--color-gray-700,#374151))] rounded mb-2" />
384
+ <div class="h-3 w-48 bg-[color:var(--mcp-ui-surface-muted,var(--color-gray-200,#e5e7eb))] dark:bg-[color:var(--mcp-ui-surface-muted,var(--color-gray-700,#374151))] rounded" />
371
385
  </div>
372
386
  }
373
387
  >
374
- <div class="w-full p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
375
- <p class="text-sm font-medium text-yellow-900 dark:text-yellow-100">
388
+ <div class="w-full p-4 bg-[color:var(--mcp-ui-warning-soft,var(--color-yellow-50))] dark:bg-[color:var(--mcp-ui-warning-soft,color-mix(in_oklab,var(--color-yellow-900)_20%,transparent))] border border-[color:var(--mcp-ui-warning-border,var(--color-yellow-200))] dark:border-[color:var(--mcp-ui-warning-border,var(--color-yellow-800))] rounded-lg">
389
+ <p class="text-sm font-medium text-[color:var(--mcp-ui-warning,var(--color-yellow-900))] dark:text-[color:var(--mcp-ui-warning,var(--color-yellow-100))]">
376
390
  {strings.graphUnavailable}
377
391
  </p>
378
- <p class="text-xs text-yellow-700 dark:text-yellow-300 mt-1">
392
+ <p class="text-xs text-[color:var(--mcp-ui-warning,var(--color-yellow-700))] dark:text-[color:var(--mcp-ui-warning,var(--color-yellow-300))] mt-1">
379
393
  Install <code>@antv/g6</code> peer dependency to render <code>type: "graph"</code>{' '}
380
394
  components.
381
395
  </p>
@@ -400,7 +414,7 @@ export const GraphRenderer: Component<GraphRendererProps> = (props) => {
400
414
  ref={exportTriggerRef}
401
415
  type="button"
402
416
  onClick={() => setExportMenuOpen((v) => !v)}
403
- class="px-2 py-1 text-xs bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
417
+ class="px-2 py-1 text-xs 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 shadow-[var(--mcp-ui-shadow,var(--shadow-sm))] hover:bg-[color:var(--mcp-ui-surface-hover,var(--color-gray-50,#f9fafb))] dark:hover:bg-[color:var(--mcp-ui-surface-hover,var(--color-gray-700,#374151))] text-[color:var(--mcp-ui-fg,var(--color-gray-700,#374151))] dark:text-[color:var(--mcp-ui-fg,var(--color-gray-300,#d1d5db))]"
404
418
  title={strings.graphExport}
405
419
  aria-label={strings.graphExport}
406
420
  aria-haspopup="menu"
@@ -438,10 +452,12 @@ export const GraphRenderer: Component<GraphRendererProps> = (props) => {
438
452
  <button
439
453
  type="button"
440
454
  onClick={item.onClick}
441
- class="w-full text-left px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 border-b border-gray-100 dark:border-gray-700 last:border-b-0"
455
+ class="w-full text-left px-3 py-2 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))] text-[color:var(--mcp-ui-fg,var(--color-gray-700,#374151))] dark:text-[color:var(--mcp-ui-fg,var(--color-gray-300,#d1d5db))] border-b border-[color:var(--mcp-ui-border,var(--color-gray-100,#f3f4f6))] dark:border-[color:var(--mcp-ui-border,var(--color-gray-700,#374151))] last:border-b-0"
442
456
  >
443
457
  <div class="font-medium">{item.label}</div>
444
- <div class="text-[10px] text-gray-500 dark:text-gray-400">{item.hint}</div>
458
+ <div class="text-[10px] 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))]">
459
+ {item.hint}
460
+ </div>
445
461
  </button>
446
462
  )}
447
463
  </For>
@@ -451,7 +467,7 @@ export const GraphRenderer: Component<GraphRendererProps> = (props) => {
451
467
  {/* Native G6 canvas — hidden once a render error degrades us. */}
452
468
  <div
453
469
  ref={containerRef}
454
- class={`bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden ${
470
+ class={`mcp-ui-root 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 ${
455
471
  error() ? 'hidden' : ''
456
472
  } ${isExpanded() ? 'flex-1 min-h-0' : ''}`}
457
473
  style={
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The visible and accessible marks of a missing value (v6.24.0).
3
+ *
4
+ * A table cell with no value has always shown `-`. On its own that glyph says
5
+ * nothing to a screen reader (it is read as "dash", or skipped) and it is easy
6
+ * to take for a zero or a minus sign. These two components give it a meaning:
7
+ *
8
+ * - {@link MissingValueMark} — the cell content: the `-` hidden from
9
+ * assistive technology, followed by `strings.missingValue` hidden from
10
+ * sight. Visually-hidden through an inline style rather than Tailwind's
11
+ * `sr-only`, so the text cannot leak on a host whose Tailwind build does
12
+ * not scan this package.
13
+ * - {@link MissingValueLegend} — one line under a table or data view that
14
+ * contains a missing value, so the convention never has to be guessed.
15
+ *
16
+ * Both are SSR-safe: no browser API, and the same markup on server and client.
17
+ */
18
+
19
+ import type { Component, JSX } from 'solid-js'
20
+ import { formatMCPUIString, useMCPUIStrings } from '../context/MCPUIStringsContext'
21
+ import { MISSING_VALUE_MARKER } from '../utils/missing-value'
22
+
23
+ /** The classic visually-hidden recipe: in the accessibility tree, out of sight. */
24
+ export const VISUALLY_HIDDEN_STYLE: JSX.CSSProperties = {
25
+ position: 'absolute',
26
+ width: '1px',
27
+ height: '1px',
28
+ padding: '0',
29
+ margin: '-1px',
30
+ overflow: 'hidden',
31
+ clip: 'rect(0, 0, 0, 0)',
32
+ 'white-space': 'nowrap',
33
+ border: '0',
34
+ }
35
+
36
+ /** Content of a cell whose value is missing. */
37
+ export const MissingValueMark: Component = () => {
38
+ const strings = useMCPUIStrings()
39
+ return (
40
+ <span data-mcp-missing-value="">
41
+ <span aria-hidden="true" class="text-[color:var(--mcp-ui-fg-subtle,var(--color-gray-400,#9ca3af))] dark:text-[color:var(--mcp-ui-fg-muted,var(--color-gray-500,#6b7280))]">
42
+ {MISSING_VALUE_MARKER}
43
+ </span>
44
+ <span style={VISUALLY_HIDDEN_STYLE}>{strings.missingValue}</span>
45
+ </span>
46
+ )
47
+ }
48
+
49
+ /** The legend line explaining the missing-value glyph. */
50
+ export const MissingValueLegend: Component<{ class?: string }> = (props) => {
51
+ const strings = useMCPUIStrings()
52
+ return (
53
+ <p
54
+ data-mcp-missing-legend=""
55
+ role="note"
56
+ class={`text-xs 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))] ${props.class ?? ''}`}
57
+ >
58
+ {formatMCPUIString(strings.missingValueLegend, { marker: MISSING_VALUE_MARKER })}
59
+ </p>
60
+ )
61
+ }
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { Component, Show, createSignal, createEffect, onCleanup, JSX } from 'solid-js'
7
7
  import { Portal } from 'solid-js/web'
8
+ import { bindPortalTheme } from '../utils/theme'
8
9
  import type { UIComponent, ModalComponentParams } from '../types'
9
10
  import { useMCPUIStrings } from '../context/MCPUIStringsContext'
10
11
 
@@ -75,6 +76,7 @@ export interface ModalRendererProps {
75
76
  export const ModalRenderer: Component<ModalRendererProps> = (props) => {
76
77
  const strings = useMCPUIStrings()
77
78
  const [isVisible, setIsVisible] = createSignal(false)
79
+ let themeAnchor: HTMLSpanElement | undefined
78
80
 
79
81
  const params = () => props.params || (props.component?.params as ModalComponentParams) || {}
80
82
 
@@ -151,11 +153,14 @@ export const ModalRenderer: Component<ModalRendererProps> = (props) => {
151
153
  }
152
154
 
153
155
  return (
156
+ <>
157
+ <span hidden aria-hidden="true" ref={themeAnchor} />
154
158
  <Show when={isVisible()}>
155
159
  <Portal>
156
160
  {/* Backdrop */}
157
161
  <div
158
- class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm"
162
+ ref={(element) => bindPortalTheme(element, () => themeAnchor)}
163
+ class="mcp-ui-root fixed inset-0 z-50 flex items-center justify-center p-4 bg-[color:var(--mcp-ui-overlay,color-mix(in_oklab,var(--color-black,#000)_50%,transparent))] backdrop-blur-sm"
159
164
  style={{ animation: 'modal-fade-in 0.15s ease-out' }}
160
165
  onClick={handleBackdropClick}
161
166
  role="dialog"
@@ -165,17 +170,17 @@ export const ModalRenderer: Component<ModalRendererProps> = (props) => {
165
170
  >
166
171
  {/* Modal container */}
167
172
  <div
168
- class={`relative w-full ${sizeClass()} bg-white dark:bg-gray-800 rounded-lg shadow-xl`}
173
+ class={`relative w-full ${sizeClass()} 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-xl))]`}
169
174
  style={{ animation: 'modal-scale-in 0.15s ease-out' }}
170
175
  onClick={(e) => e.stopPropagation()}
171
176
  >
172
177
  {/* Header */}
173
178
  <Show when={params()?.title || params()?.showClose !== false}>
174
- <div class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
179
+ <div class="flex items-center justify-between p-4 border-b border-[color:var(--mcp-ui-border,var(--color-gray-200,#e5e7eb))] dark:border-[color:var(--mcp-ui-border,var(--color-gray-700,#374151))]">
175
180
  <Show when={params()?.title}>
176
181
  <h2
177
182
  id="modal-title"
178
- class="text-lg font-semibold text-gray-900 dark:text-white"
183
+ class="text-lg font-semibold text-[color:var(--mcp-ui-fg,var(--color-gray-900,#111827))] dark:text-[color:var(--mcp-ui-fg,var(--color-white,#fff))]"
179
184
  >
180
185
  {params()!.title}
181
186
  </h2>
@@ -186,7 +191,7 @@ export const ModalRenderer: Component<ModalRendererProps> = (props) => {
186
191
  <Show when={params()?.showClose !== false}>
187
192
  <button
188
193
  onClick={handleClose}
189
- class="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
194
+ class="p-1 text-[color:var(--mcp-ui-fg-subtle,var(--color-gray-400,#9ca3af))] hover:text-[color:var(--mcp-ui-fg-muted,var(--color-gray-600,#4b5563))] dark:hover:text-[color:var(--mcp-ui-fg,var(--color-gray-300,#d1d5db))] 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-colors"
190
195
  aria-label={strings.modalClose}
191
196
  >
192
197
  <svg
@@ -230,5 +235,6 @@ export const ModalRenderer: Component<ModalRendererProps> = (props) => {
230
235
  `}</style>
231
236
  </Portal>
232
237
  </Show>
238
+ </>
233
239
  )
234
240
  }
@@ -22,6 +22,7 @@
22
22
 
23
23
  import { Component, JSX, Show, createSignal, createEffect, onCleanup } from 'solid-js'
24
24
  import { Portal } from 'solid-js/web'
25
+ import { bindPortalTheme } from '../utils/theme'
25
26
 
26
27
  export interface PortalDropdownMenuProps {
27
28
  /**
@@ -111,7 +112,10 @@ export const PortalDropdownMenu: Component<PortalDropdownMenuProps> = (props) =>
111
112
  <Show when={props.open}>
112
113
  <Portal>
113
114
  <div
114
- ref={menuRef}
115
+ ref={(element) => {
116
+ menuRef = element
117
+ bindPortalTheme(element, () => props.trigger)
118
+ }}
115
119
  role="menu"
116
120
  data-mcp-ui-portal="menu"
117
121
  style={{
@@ -121,7 +125,7 @@ export const PortalDropdownMenu: Component<PortalDropdownMenuProps> = (props) =>
121
125
  'z-index': 9999,
122
126
  width: `${props.width ?? 144}px`,
123
127
  }}
124
- class={`bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg py-1 text-sm ${props.class ?? ''}`}
128
+ class={`mcp-ui-root 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-lg shadow-[var(--mcp-ui-shadow,var(--shadow-lg))] py-1 text-sm ${props.class ?? ''}`}
125
129
  >
126
130
  {props.children}
127
131
  </div>