@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.
Files changed (124) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +47 -0
  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 +216 -6
  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 +217 -7
  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 +1 -1
  30. package/dist/components/DegradedFallback.cjs.map +1 -1
  31. package/dist/components/DegradedFallback.js +1 -1
  32. package/dist/components/DegradedFallback.js.map +1 -1
  33. package/dist/components/ExpandableWrapper.cjs +10 -7
  34. package/dist/components/ExpandableWrapper.cjs.map +1 -1
  35. package/dist/components/ExpandableWrapper.d.ts.map +1 -1
  36. package/dist/components/ExpandableWrapper.js +10 -7
  37. package/dist/components/ExpandableWrapper.js.map +1 -1
  38. package/dist/components/FormFieldRenderer.cjs +13 -12
  39. package/dist/components/FormFieldRenderer.cjs.map +1 -1
  40. package/dist/components/FormFieldRenderer.d.ts.map +1 -1
  41. package/dist/components/FormFieldRenderer.js +13 -12
  42. package/dist/components/FormFieldRenderer.js.map +1 -1
  43. package/dist/components/FormRenderer.cjs +1 -1
  44. package/dist/components/FormRenderer.cjs.map +1 -1
  45. package/dist/components/FormRenderer.js +1 -1
  46. package/dist/components/FormRenderer.js.map +1 -1
  47. package/dist/components/GenerativeUIErrorBoundary.cjs +1 -1
  48. package/dist/components/GenerativeUIErrorBoundary.cjs.map +1 -1
  49. package/dist/components/GenerativeUIErrorBoundary.js +1 -1
  50. package/dist/components/GenerativeUIErrorBoundary.js.map +1 -1
  51. package/dist/components/GhostText.cjs +2 -2
  52. package/dist/components/GhostText.cjs.map +1 -1
  53. package/dist/components/GhostText.js +2 -2
  54. package/dist/components/GhostText.js.map +1 -1
  55. package/dist/components/GraphRenderer.cjs +100 -29
  56. package/dist/components/GraphRenderer.cjs.map +1 -1
  57. package/dist/components/GraphRenderer.d.ts +1 -1
  58. package/dist/components/GraphRenderer.d.ts.map +1 -1
  59. package/dist/components/GraphRenderer.js +100 -29
  60. package/dist/components/GraphRenderer.js.map +1 -1
  61. package/dist/components/MissingValue.cjs +2 -2
  62. package/dist/components/MissingValue.cjs.map +1 -1
  63. package/dist/components/MissingValue.js +2 -2
  64. package/dist/components/MissingValue.js.map +1 -1
  65. package/dist/components/ModalRenderer.cjs +39 -31
  66. package/dist/components/ModalRenderer.cjs.map +1 -1
  67. package/dist/components/ModalRenderer.d.ts.map +1 -1
  68. package/dist/components/ModalRenderer.js +40 -32
  69. package/dist/components/ModalRenderer.js.map +1 -1
  70. package/dist/components/PortalDropdownMenu.cjs +6 -3
  71. package/dist/components/PortalDropdownMenu.cjs.map +1 -1
  72. package/dist/components/PortalDropdownMenu.d.ts.map +1 -1
  73. package/dist/components/PortalDropdownMenu.js +7 -4
  74. package/dist/components/PortalDropdownMenu.js.map +1 -1
  75. package/dist/components/ScratchpadPanel.cjs +38 -44
  76. package/dist/components/ScratchpadPanel.cjs.map +1 -1
  77. package/dist/components/ScratchpadPanel.d.ts.map +1 -1
  78. package/dist/components/ScratchpadPanel.js +38 -44
  79. package/dist/components/ScratchpadPanel.js.map +1 -1
  80. package/dist/components/UIResourceRenderer.cjs +20 -20
  81. package/dist/components/UIResourceRenderer.cjs.map +1 -1
  82. package/dist/components/UIResourceRenderer.js +20 -20
  83. package/dist/components/UIResourceRenderer.js.map +1 -1
  84. package/dist/components/chart-missing-plugin.cjs +4 -4
  85. package/dist/components/chart-missing-plugin.cjs.map +1 -1
  86. package/dist/components/chart-missing-plugin.d.ts +3 -1
  87. package/dist/components/chart-missing-plugin.d.ts.map +1 -1
  88. package/dist/components/chart-missing-plugin.js +4 -4
  89. package/dist/components/chart-missing-plugin.js.map +1 -1
  90. package/dist/utils/math-markdown.cjs +1 -1
  91. package/dist/utils/math-markdown.cjs.map +1 -1
  92. package/dist/utils/math-markdown.js +1 -1
  93. package/dist/utils/math-markdown.js.map +1 -1
  94. package/dist/utils/theme.cjs +91 -0
  95. package/dist/utils/theme.cjs.map +1 -0
  96. package/dist/utils/theme.d.ts +11 -0
  97. package/dist/utils/theme.d.ts.map +1 -0
  98. package/dist/utils/theme.js +91 -0
  99. package/dist/utils/theme.js.map +1 -0
  100. package/docs/SECURITY-DEPENDENCIES.md +81 -0
  101. package/docs/THEMING.md +205 -0
  102. package/package.json +5 -3
  103. package/src/components/ActionGroupRenderer.tsx +6 -6
  104. package/src/components/AutocompleteDropdown.tsx +17 -17
  105. package/src/components/AutocompleteFormField.tsx +10 -9
  106. package/src/components/ChartJSRenderer.tsx +483 -239
  107. package/src/components/ChatPrompt.tsx +16 -16
  108. package/src/components/ComponentToolbar.tsx +5 -5
  109. package/src/components/DegradedFallback.tsx +9 -9
  110. package/src/components/ExpandableWrapper.tsx +15 -11
  111. package/src/components/FormFieldRenderer.tsx +52 -51
  112. package/src/components/FormRenderer.tsx +12 -12
  113. package/src/components/GenerativeUIErrorBoundary.tsx +6 -6
  114. package/src/components/GhostText.tsx +7 -7
  115. package/src/components/GraphRenderer.tsx +83 -67
  116. package/src/components/MissingValue.tsx +2 -2
  117. package/src/components/ModalRenderer.tsx +11 -5
  118. package/src/components/PortalDropdownMenu.tsx +6 -2
  119. package/src/components/ScratchpadPanel.tsx +144 -141
  120. package/src/components/UIResourceRenderer.tsx +101 -101
  121. package/src/components/chart-missing-plugin.ts +5 -3
  122. package/src/styles/autocomplete.css +53 -53
  123. package/src/utils/math-markdown.ts +1 -1
  124. package/src/utils/theme.ts +101 -0
@@ -262,9 +262,9 @@ export const FormRenderer: Component<FormRendererProps> = (props) => {
262
262
  }
263
263
 
264
264
  return (
265
- <div class="w-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4">
265
+ <div class="mcp-ui-root 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))] p-4">
266
266
  <Show when={params().title}>
267
- <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
267
+ <h3 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))] mb-4">
268
268
  {params().title}
269
269
  </h3>
270
270
  </Show>
@@ -276,7 +276,7 @@ export const FormRenderer: Component<FormRendererProps> = (props) => {
276
276
  const prefilled = params().fields.filter((f) => f.prefill != null).length
277
277
  const total = params().fields.length
278
278
  return (
279
- <p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
279
+ <p 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))] mb-3">
280
280
  {formatMCPUIString(
281
281
  prefilled === 1 ? strings.formPrefilledOne : strings.formPrefilledMany,
282
282
  { count: prefilled, total }
@@ -301,8 +301,8 @@ export const FormRenderer: Component<FormRendererProps> = (props) => {
301
301
  </div>
302
302
 
303
303
  <Show when={errors()._form}>
304
- <div class="mt-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md">
305
- <p class="text-sm text-red-600 dark:text-red-400" role="alert">
304
+ <div class="mt-4 p-3 bg-[color:var(--mcp-ui-danger-soft,var(--color-red-50))] dark:bg-[color:var(--mcp-ui-danger-soft,color-mix(in_oklab,var(--color-red-900)_20%,transparent))] border border-[color:var(--mcp-ui-danger-border,var(--color-red-200))] dark:border-[color:var(--mcp-ui-danger-border,var(--color-red-800))] rounded-md">
305
+ <p class="text-sm text-[color:var(--mcp-ui-danger,var(--color-red-600))] dark:text-[color:var(--mcp-ui-danger,var(--color-red-400))]" role="alert">
306
306
  {errors()._form}
307
307
  </p>
308
308
  </div>
@@ -310,8 +310,8 @@ export const FormRenderer: Component<FormRendererProps> = (props) => {
310
310
 
311
311
  {/* Auto-submit countdown (v4.2.0) */}
312
312
  <Show when={countdown() != null}>
313
- <div class="mt-4 flex items-center gap-3 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md">
314
- <span class="text-sm text-blue-700 dark:text-blue-300">
313
+ <div class="mt-4 flex items-center gap-3 p-3 bg-[color:var(--mcp-ui-accent-soft,var(--color-blue-50))] dark:bg-[color:var(--mcp-ui-accent-soft,color-mix(in_oklab,var(--color-blue-900)_20%,transparent))] border border-[color:var(--mcp-ui-accent-border,var(--color-blue-200))] dark:border-[color:var(--mcp-ui-accent-border,var(--color-blue-800))] rounded-md">
314
+ <span class="text-sm text-[color:var(--mcp-ui-accent,var(--color-blue-700))] dark:text-[color:var(--mcp-ui-accent,var(--color-blue-300))]">
315
315
  {formatMCPUIString(strings.formSubmitCountdown, {
316
316
  label: params().submitLabel || strings.submit,
317
317
  seconds: countdown()!,
@@ -320,22 +320,22 @@ export const FormRenderer: Component<FormRendererProps> = (props) => {
320
320
  <button
321
321
  type="button"
322
322
  onClick={() => { cancelCountdown(); setUserInteracted(true) }}
323
- class="text-sm text-blue-600 dark:text-blue-400 underline hover:text-blue-800 dark:hover:text-blue-200"
323
+ class="text-sm text-[color:var(--mcp-ui-accent,var(--color-blue-600))] dark:text-[color:var(--mcp-ui-accent,var(--color-blue-400))] underline hover:text-[color:var(--mcp-ui-accent,var(--color-blue-800))] dark:hover:text-[color:var(--mcp-ui-accent,var(--color-blue-200))]"
324
324
  >
325
325
  {strings.cancel}
326
326
  </button>
327
327
  </div>
328
328
  </Show>
329
329
 
330
- <div class="flex gap-2 pt-4 mt-4 border-t border-gray-200 dark:border-gray-700">
330
+ <div class="flex gap-2 pt-4 mt-4 border-t border-[color:var(--mcp-ui-border,var(--color-gray-200,#e5e7eb))] dark:border-[color:var(--mcp-ui-border,var(--color-gray-700,#374151))]">
331
331
  <button
332
332
  type="submit"
333
333
  disabled={isSubmitting()}
334
- class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
334
+ class="px-4 py-2 bg-[color:var(--mcp-ui-accent,var(--color-blue-600))] text-[color:var(--mcp-ui-accent-fg,var(--color-white,#fff))] rounded-md hover:bg-[color:var(--mcp-ui-accent-hover,var(--color-blue-700))] focus:outline-none focus:ring-2 focus:ring-[color:var(--mcp-ui-focus,var(--color-blue-500))] focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
335
335
  >
336
336
  {isSubmitting() ? (
337
337
  <span class="flex items-center gap-2">
338
- <span class="animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full" />
338
+ <span class="animate-spin h-4 w-4 border-2 border-[color:var(--mcp-ui-border,var(--color-white,#fff))] border-t-transparent rounded-full" />
339
339
  {strings.formSubmitting}
340
340
  </span>
341
341
  ) : (
@@ -347,7 +347,7 @@ export const FormRenderer: Component<FormRendererProps> = (props) => {
347
347
  type="button"
348
348
  onClick={() => initializeForm(true)}
349
349
  disabled={isSubmitting()}
350
- class="px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
350
+ class="px-4 py-2 border border-[color:var(--mcp-ui-border,var(--color-gray-300,#d1d5db))] dark:border-[color:var(--mcp-ui-border,var(--color-gray-600,#4b5563))] text-[color:var(--mcp-ui-fg,var(--color-gray-700,#374151))] dark:text-[color:var(--mcp-ui-fg,var(--color-gray-300,#d1d5db))] rounded-md 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))] focus:outline-none focus:ring-2 focus:ring-[color:var(--mcp-ui-focus,var(--color-gray-500,#6b7280))] focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
351
351
  >
352
352
  {strings.formReset}
353
353
  </button>
@@ -66,11 +66,11 @@ function DefaultErrorFallback(props: {
66
66
  }) {
67
67
  const strings = useMCPUIStrings()
68
68
  return (
69
- <div class="w-full h-full bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
69
+ <div class="w-full h-full 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 p-4">
70
70
  <div class="flex items-start gap-3">
71
71
  <div class="flex-shrink-0">
72
72
  <svg
73
- class="w-5 h-5 text-yellow-600 dark:text-yellow-400"
73
+ class="w-5 h-5 text-[color:var(--mcp-ui-warning,var(--color-yellow-600))] dark:text-[color:var(--mcp-ui-warning,var(--color-yellow-400))]"
74
74
  fill="none"
75
75
  stroke="currentColor"
76
76
  viewBox="0 0 24 24"
@@ -84,24 +84,24 @@ function DefaultErrorFallback(props: {
84
84
  </svg>
85
85
  </div>
86
86
  <div class="flex-1 min-w-0">
87
- <p class="text-sm font-medium text-yellow-900 dark:text-yellow-100">
87
+ <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))]">
88
88
  {strings.errorBoundaryTitle}
89
89
  </p>
90
- <p class="text-xs text-yellow-700 dark:text-yellow-300 mt-1">
90
+ <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">
91
91
  {formatMCPUIString(strings.errorBoundaryMeta, {
92
92
  type: props.componentType || strings.unknown,
93
93
  id: props.componentId?.slice(0, 8) || strings.unknown,
94
94
  })}
95
95
  </p>
96
96
  <Show when={import.meta.env.DEV}>
97
- <p class="text-xs text-yellow-600 dark:text-yellow-400 mt-2 font-mono">
97
+ <p class="text-xs text-[color:var(--mcp-ui-warning,var(--color-yellow-600))] dark:text-[color:var(--mcp-ui-warning,var(--color-yellow-400))] mt-2 font-mono">
98
98
  {props.error.message}
99
99
  </p>
100
100
  </Show>
101
101
  <Show when={props.allowRetry}>
102
102
  <button
103
103
  onClick={props.onRetry}
104
- class="mt-3 text-xs font-medium text-yellow-800 dark:text-yellow-200 hover:text-yellow-900 dark:hover:text-yellow-100 underline"
104
+ class="mt-3 text-xs font-medium text-[color:var(--mcp-ui-warning,var(--color-yellow-800))] dark:text-[color:var(--mcp-ui-warning,var(--color-yellow-200))] hover:text-[color:var(--mcp-ui-warning,var(--color-yellow-900))] dark:hover:text-[color:var(--mcp-ui-warning,var(--color-yellow-100))] underline"
105
105
  >
106
106
  {strings.errorBoundaryRetry}
107
107
  </button>
@@ -58,7 +58,7 @@ export const GhostText: Component<GhostTextProps> = (props) => {
58
58
 
59
59
  return (
60
60
  <div
61
- class={`mcp-ghost-text-container ${props.class || ''}`}
61
+ class={`mcp-ui-root mcp-ghost-text-container ${props.class || ''}`}
62
62
  style={{
63
63
  position: 'relative',
64
64
  display: 'inline-block',
@@ -91,7 +91,7 @@ export const GhostText: Component<GhostTextProps> = (props) => {
91
91
  <span
92
92
  class={`mcp-ghost-text ${props.ghostClass || ''}`}
93
93
  style={{
94
- color: '#9ca3af',
94
+ color: 'var(--mcp-ui-fg-subtle, #9ca3af)',
95
95
  opacity: '0.7'
96
96
  }}
97
97
  >
@@ -110,8 +110,8 @@ export const GhostText: Component<GhostTextProps> = (props) => {
110
110
  top: '50%',
111
111
  transform: 'translateY(-50%)',
112
112
  'font-size': '0.75rem',
113
- color: '#6b7280',
114
- 'background-color': 'rgba(255, 255, 255, 0.9)',
113
+ color: 'var(--mcp-ui-fg-muted, #6b7280)',
114
+ 'background-color': 'var(--mcp-ui-surface, rgba(255, 255, 255, 0.9))',
115
115
  padding: '2px 6px',
116
116
  'border-radius': '4px',
117
117
  'pointer-events': 'none'
@@ -137,8 +137,8 @@ export const GhostText: Component<GhostTextProps> = (props) => {
137
137
  display: 'inline-block',
138
138
  width: '12px',
139
139
  height: '12px',
140
- border: '2px solid #e5e7eb',
141
- 'border-top-color': '#3b82f6',
140
+ border: '2px solid var(--mcp-ui-border, #e5e7eb)',
141
+ 'border-top-color': 'var(--mcp-ui-accent, #3b82f6)',
142
142
  'border-radius': '50%',
143
143
  animation: 'mcp-spin 0.6s linear infinite'
144
144
  }}
@@ -230,7 +230,7 @@ export interface GhostTextInputProps {
230
230
  */
231
231
  export const GhostTextInput: Component<GhostTextInputProps> = (props) => {
232
232
  return (
233
- <div class="mcp-ghost-text-input-wrapper" style={{ position: 'relative' }}>
233
+ <div class="mcp-ui-root mcp-ghost-text-input-wrapper" style={{ position: 'relative' }}>
234
234
  <GhostText
235
235
  inputValue={props.value}
236
236
  ghostText={props.ghostText}
@@ -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={
@@ -38,7 +38,7 @@ export const MissingValueMark: Component = () => {
38
38
  const strings = useMCPUIStrings()
39
39
  return (
40
40
  <span data-mcp-missing-value="">
41
- <span aria-hidden="true" class="text-gray-400 dark:text-gray-500">
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
42
  {MISSING_VALUE_MARKER}
43
43
  </span>
44
44
  <span style={VISUALLY_HIDDEN_STYLE}>{strings.missingValue}</span>
@@ -53,7 +53,7 @@ export const MissingValueLegend: Component<{ class?: string }> = (props) => {
53
53
  <p
54
54
  data-mcp-missing-legend=""
55
55
  role="note"
56
- class={`text-xs text-gray-500 dark:text-gray-400 ${props.class ?? ''}`}
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
57
  >
58
58
  {formatMCPUIString(strings.missingValueLegend, { marker: MISSING_VALUE_MARKER })}
59
59
  </p>
@@ -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>