@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
@@ -37,7 +37,7 @@
37
37
  }
38
38
 
39
39
  .mcp-ghost-text {
40
- color: #9ca3af;
40
+ color: var(--mcp-ui-fg-subtle, #9ca3af);
41
41
  opacity: 0.7;
42
42
  }
43
43
 
@@ -47,11 +47,11 @@
47
47
  top: 50%;
48
48
  transform: translateY(-50%);
49
49
  font-size: 0.75rem;
50
- color: #6b7280;
51
- background-color: rgba(255, 255, 255, 0.95);
50
+ color: var(--mcp-ui-fg-muted, #6b7280);
51
+ background-color: var(--mcp-ui-surface, rgba(255, 255, 255, 0.95));
52
52
  padding: 2px 8px;
53
53
  border-radius: 4px;
54
- border: 1px solid #e5e7eb;
54
+ border: 1px solid var(--mcp-ui-border, #e5e7eb);
55
55
  pointer-events: none;
56
56
  animation: mcp-hint-fade-in 150ms ease;
57
57
  }
@@ -86,10 +86,10 @@
86
86
  left: 0;
87
87
  right: 0;
88
88
  z-index: 50;
89
- background-color: #ffffff;
90
- border: 1px solid #e5e7eb;
89
+ background-color: var(--mcp-ui-surface, #ffffff);
90
+ border: 1px solid var(--mcp-ui-border, #e5e7eb);
91
91
  border-radius: 6px;
92
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
92
+ box-shadow: var(--mcp-ui-shadow, 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06));
93
93
  overflow: hidden;
94
94
  animation: mcp-dropdown-enter 150ms ease;
95
95
  }
@@ -120,11 +120,11 @@
120
120
  }
121
121
 
122
122
  .mcp-autocomplete-option:hover {
123
- background-color: #f3f4f6;
123
+ background-color: var(--mcp-ui-surface-hover, #f3f4f6);
124
124
  }
125
125
 
126
126
  .mcp-autocomplete-option-selected {
127
- background-color: #eff6ff !important;
127
+ background-color: var(--mcp-ui-accent-soft, #eff6ff) !important;
128
128
  }
129
129
 
130
130
  .mcp-autocomplete-option-disabled {
@@ -159,7 +159,7 @@
159
159
  .mcp-autocomplete-option-label {
160
160
  display: block;
161
161
  font-size: 0.875rem;
162
- color: #374151;
162
+ color: var(--mcp-ui-fg, #374151);
163
163
  white-space: nowrap;
164
164
  overflow: hidden;
165
165
  text-overflow: ellipsis;
@@ -168,7 +168,7 @@
168
168
  .mcp-autocomplete-option-description {
169
169
  display: block;
170
170
  font-size: 0.75rem;
171
- color: #6b7280;
171
+ color: var(--mcp-ui-fg-muted, #6b7280);
172
172
  white-space: nowrap;
173
173
  overflow: hidden;
174
174
  text-overflow: ellipsis;
@@ -177,14 +177,14 @@
177
177
 
178
178
  .mcp-autocomplete-highlight {
179
179
  font-weight: 600;
180
- color: #1d4ed8;
180
+ color: var(--mcp-ui-accent, #1d4ed8);
181
181
  }
182
182
 
183
183
  .mcp-autocomplete-loading,
184
184
  .mcp-autocomplete-empty {
185
185
  padding: 12px 16px;
186
186
  font-size: 0.875rem;
187
- color: #6b7280;
187
+ color: var(--mcp-ui-fg-muted, #6b7280);
188
188
  }
189
189
 
190
190
  .mcp-autocomplete-loading {
@@ -199,10 +199,10 @@
199
199
 
200
200
  .mcp-autocomplete-footer {
201
201
  padding: 6px 12px;
202
- border-top: 1px solid #e5e7eb;
203
- background-color: #f9fafb;
202
+ border-top: 1px solid var(--mcp-ui-border, #e5e7eb);
203
+ background-color: var(--mcp-ui-surface-muted, #f9fafb);
204
204
  font-size: 0.75rem;
205
- color: #6b7280;
205
+ color: var(--mcp-ui-fg-muted, #6b7280);
206
206
  }
207
207
 
208
208
  /* =============================================================================
@@ -211,107 +211,107 @@
211
211
 
212
212
  @media (prefers-color-scheme: dark) {
213
213
  .mcp-ghost-text {
214
- color: #6b7280;
214
+ color: var(--mcp-ui-fg-muted, #6b7280);
215
215
  }
216
216
 
217
217
  .mcp-ghost-text-hint {
218
- background-color: rgba(31, 41, 55, 0.95);
219
- border-color: #374151;
220
- color: #9ca3af;
218
+ background-color: var(--mcp-ui-surface, rgba(31, 41, 55, 0.95));
219
+ border-color: var(--mcp-ui-border, #374151);
220
+ color: var(--mcp-ui-fg-subtle, #9ca3af);
221
221
  }
222
222
 
223
223
  .mcp-autocomplete-dropdown {
224
- background-color: #1f2937;
225
- border-color: #374151;
224
+ background-color: var(--mcp-ui-surface, #1f2937);
225
+ border-color: var(--mcp-ui-border, #374151);
226
226
  }
227
227
 
228
228
  .mcp-autocomplete-option:hover {
229
- background-color: #374151;
229
+ background-color: var(--mcp-ui-surface-hover, #374151);
230
230
  }
231
231
 
232
232
  .mcp-autocomplete-option-selected {
233
- background-color: #1e3a5f !important;
233
+ background-color: var(--mcp-ui-accent-soft, #1e3a5f) !important;
234
234
  }
235
235
 
236
236
  .mcp-autocomplete-option-label {
237
- color: #e5e7eb;
237
+ color: var(--mcp-ui-fg, #e5e7eb);
238
238
  }
239
239
 
240
240
  .mcp-autocomplete-option-description {
241
- color: #9ca3af;
241
+ color: var(--mcp-ui-fg-subtle, #9ca3af);
242
242
  }
243
243
 
244
244
  .mcp-autocomplete-highlight {
245
- color: #60a5fa;
245
+ color: var(--mcp-ui-accent, #60a5fa);
246
246
  }
247
247
 
248
248
  .mcp-autocomplete-loading,
249
249
  .mcp-autocomplete-empty {
250
- color: #9ca3af;
250
+ color: var(--mcp-ui-fg-subtle, #9ca3af);
251
251
  }
252
252
 
253
253
  .mcp-autocomplete-footer {
254
- background-color: #111827;
255
- border-color: #374151;
256
- color: #9ca3af;
254
+ background-color: var(--mcp-ui-surface-muted, #111827);
255
+ border-color: var(--mcp-ui-border, #374151);
256
+ color: var(--mcp-ui-fg-subtle, #9ca3af);
257
257
  }
258
258
 
259
259
  .mcp-autocomplete-footer kbd {
260
- background-color: #374151;
261
- color: #d1d5db;
260
+ background-color: var(--mcp-ui-surface-hover, #374151);
261
+ color: var(--mcp-ui-fg, #d1d5db);
262
262
  }
263
263
  }
264
264
 
265
265
  /* Explicit dark mode class support */
266
266
  .dark .mcp-ghost-text {
267
- color: #6b7280;
267
+ color: var(--mcp-ui-fg-muted, #6b7280);
268
268
  }
269
269
 
270
270
  .dark .mcp-ghost-text-hint {
271
- background-color: rgba(31, 41, 55, 0.95);
272
- border-color: #374151;
273
- color: #9ca3af;
271
+ background-color: var(--mcp-ui-surface, rgba(31, 41, 55, 0.95));
272
+ border-color: var(--mcp-ui-border, #374151);
273
+ color: var(--mcp-ui-fg-subtle, #9ca3af);
274
274
  }
275
275
 
276
276
  .dark .mcp-autocomplete-dropdown {
277
- background-color: #1f2937;
278
- border-color: #374151;
277
+ background-color: var(--mcp-ui-surface, #1f2937);
278
+ border-color: var(--mcp-ui-border, #374151);
279
279
  }
280
280
 
281
281
  .dark .mcp-autocomplete-option:hover {
282
- background-color: #374151;
282
+ background-color: var(--mcp-ui-surface-hover, #374151);
283
283
  }
284
284
 
285
285
  .dark .mcp-autocomplete-option-selected {
286
- background-color: #1e3a5f !important;
286
+ background-color: var(--mcp-ui-accent-soft, #1e3a5f) !important;
287
287
  }
288
288
 
289
289
  .dark .mcp-autocomplete-option-label {
290
- color: #e5e7eb;
290
+ color: var(--mcp-ui-fg, #e5e7eb);
291
291
  }
292
292
 
293
293
  .dark .mcp-autocomplete-option-description {
294
- color: #9ca3af;
294
+ color: var(--mcp-ui-fg-subtle, #9ca3af);
295
295
  }
296
296
 
297
297
  .dark .mcp-autocomplete-highlight {
298
- color: #60a5fa;
298
+ color: var(--mcp-ui-accent, #60a5fa);
299
299
  }
300
300
 
301
301
  .dark .mcp-autocomplete-loading,
302
302
  .dark .mcp-autocomplete-empty {
303
- color: #9ca3af;
303
+ color: var(--mcp-ui-fg-subtle, #9ca3af);
304
304
  }
305
305
 
306
306
  .dark .mcp-autocomplete-footer {
307
- background-color: #111827;
308
- border-color: #374151;
309
- color: #9ca3af;
307
+ background-color: var(--mcp-ui-surface-muted, #111827);
308
+ border-color: var(--mcp-ui-border, #374151);
309
+ color: var(--mcp-ui-fg-subtle, #9ca3af);
310
310
  }
311
311
 
312
312
  .dark .mcp-autocomplete-footer kbd {
313
- background-color: #374151;
314
- color: #d1d5db;
313
+ background-color: var(--mcp-ui-surface-hover, #374151);
314
+ color: var(--mcp-ui-fg, #d1d5db);
315
315
  }
316
316
 
317
317
  /* =============================================================================
@@ -327,12 +327,12 @@
327
327
  }
328
328
 
329
329
  .mcp-autocomplete-options::-webkit-scrollbar-thumb {
330
- background-color: #d1d5db;
330
+ background-color: var(--mcp-ui-surface-hover, #d1d5db);
331
331
  border-radius: 3px;
332
332
  }
333
333
 
334
334
  .dark .mcp-autocomplete-options::-webkit-scrollbar-thumb {
335
- background-color: #4b5563;
335
+ background-color: var(--mcp-ui-surface-hover, #4b5563);
336
336
  }
337
337
 
338
338
  /* =============================================================================
@@ -341,7 +341,7 @@
341
341
 
342
342
  /* Focus styles */
343
343
  .mcp-autocomplete-option:focus {
344
- outline: 2px solid #3b82f6;
344
+ outline: 2px solid var(--mcp-ui-focus, #3b82f6);
345
345
  outline-offset: -2px;
346
346
  }
347
347
 
@@ -81,7 +81,13 @@ export interface ChartComponentParams {
81
81
  labels: string[]
82
82
  datasets: Array<{
83
83
  label: string
84
- data: number[] | Array<{ x: string | number; y: number; r?: number }>
84
+ /**
85
+ * `null` is an explicit missing value (v6.24.0): its label keeps its
86
+ * slot, a line breaks there, a bar shows a dashed outline — never a
87
+ * zero. Pie, doughnut and polar-area charts reject it, since they
88
+ * would draw it as a zero share.
89
+ */
90
+ data: Array<number | null> | Array<{ x: string | number; y: number | null; r?: number }>
85
91
  backgroundColor?: string | string[]
86
92
  borderColor?: string | string[]
87
93
  borderWidth?: number
@@ -135,6 +141,12 @@ export interface ChartComponentParams {
135
141
  * Custom CSS class (Sprint 7)
136
142
  */
137
143
  className?: string
144
+ /**
145
+ * Unit of every value in the chart (`'mm'`, `'km/h'`), shown as the value
146
+ * axis title (unless `options.scales` already sets one), after each tooltip
147
+ * value, and in the data-view column headers (v6.24.0). One unit per chart.
148
+ */
149
+ unit?: string
138
150
  }
139
151
 
140
152
  /**
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import { formatMCPUIString } from './format-string';
15
+ import { isMissingChartValue } from './missing-value';
15
16
 
16
17
  /**
17
18
  * Column headers and series names of the degraded tables.
@@ -46,10 +47,17 @@ export interface DegradedProjectionLabels {
46
47
  feature: string;
47
48
  /** Chart table — fallback dataset name. Template — `{n}`. */
48
49
  series: string;
50
+ /**
51
+ * Chart table — a dataset column header followed by the chart's `unit`.
52
+ * Template — `{label}`, `{unit}`. Only applied when the chart has a unit.
53
+ * Optional so that a complete labels table written for 6.20.0–6.23.0 still
54
+ * type-checks. @since v6.24.0
55
+ */
56
+ withUnit?: string;
49
57
  }
50
58
 
51
59
  /** English defaults for {@link DegradedProjectionLabels}. */
52
- export const DEGRADED_PROJECTION_LABELS: DegradedProjectionLabels = {
60
+ export const DEGRADED_PROJECTION_LABELS: Required<DegradedProjectionLabels> = {
53
61
  source: 'Source',
54
62
  target: 'Target',
55
63
  label: 'Label',
@@ -61,6 +69,7 @@ export const DEGRADED_PROJECTION_LABELS: DegradedProjectionLabels = {
61
69
  marker: 'marker',
62
70
  feature: 'feature',
63
71
  series: 'Series {n}',
72
+ withUnit: '{label} ({unit})',
64
73
  };
65
74
 
66
75
  const withLabels = (labels?: Partial<DegradedProjectionLabels>): DegradedProjectionLabels => ({
@@ -73,6 +82,12 @@ export interface DegradedTable {
73
82
  rows: Array<Array<string | number>>;
74
83
  }
75
84
 
85
+ /** A degraded chart table: its cells, plus which of them are missing values. */
86
+ export interface DegradedChartTable extends DegradedTable {
87
+ /** Same shape as `rows`; `true` for a dataset entry that is missing. @since v6.24.0 */
88
+ missing: boolean[][];
89
+ }
90
+
76
91
  const MAX_PROJECTED_ROWS = 200;
77
92
 
78
93
  /** Compact a value to a single table cell string. */
@@ -188,25 +203,50 @@ export function mapToDegradedTable(params: {
188
203
  * Chart → a series table: one row per label, one column per dataset. So a
189
204
  * chart that can't draw still shows its numbers. Point/object data (scatter,
190
205
  * bubble, time series) is stringified per cell.
206
+ *
207
+ * `missing` flags the cells whose dataset entry is a missing value (`null`
208
+ * or non-finite) so the table can mark them instead of showing an empty
209
+ * cell. A slot past the end of a shorter or empty dataset has no entry and is
210
+ * not flagged; a point object keeps its JSON text (where a `"y":null` reads
211
+ * as such) and is not flagged either. With a `unit`, dataset column headers
212
+ * become `labels.withUnit`.
191
213
  */
192
214
  export function chartToDegradedTable(params: {
215
+ unit?: string;
193
216
  data?: {
194
217
  labels?: Array<string | number>;
195
218
  datasets?: Array<{ label?: string; data?: unknown[] }>;
196
219
  };
197
- }, labels?: Partial<DegradedProjectionLabels>): DegradedTable {
220
+ }, labels?: Partial<DegradedProjectionLabels>): DegradedChartTable {
198
221
  const l = withLabels(labels);
222
+ const unit = typeof params.unit === 'string' && params.unit !== '' ? params.unit : undefined;
199
223
  const datasets = params.data?.datasets ?? [];
200
224
  const dataLabels = params.data?.labels ?? [];
201
225
  const rowCount = Math.max(dataLabels.length, ...datasets.map((d) => d.data?.length ?? 0), 0);
202
226
 
203
227
  const columns = [
204
228
  '',
205
- ...datasets.map((d, i) => d.label ?? formatMCPUIString(l.series, { n: i + 1 })),
229
+ ...datasets.map((d, i) => {
230
+ const name = d.label ?? formatMCPUIString(l.series, { n: i + 1 });
231
+ return unit === undefined
232
+ ? name
233
+ : formatMCPUIString(l.withUnit ?? DEGRADED_PROJECTION_LABELS.withUnit, { label: name, unit });
234
+ }),
206
235
  ];
207
236
  const rows: Array<Array<string | number>> = [];
237
+ const missing: boolean[][] = [];
208
238
  for (let r = 0; r < Math.min(rowCount, MAX_PROJECTED_ROWS); r++) {
209
239
  rows.push([cell(dataLabels[r] ?? r + 1), ...datasets.map((d) => cell(d.data?.[r]))]);
240
+ missing.push([
241
+ false,
242
+ ...datasets.map((d) => {
243
+ // Past the end of a shorter or empty dataset there is no entry, so
244
+ // nothing to flag (the cell stays empty, as before 6.24.0).
245
+ if (r >= (d.data?.length ?? 0)) return false;
246
+ const entry = d.data?.[r];
247
+ return (entry === null || typeof entry !== 'object') && isMissingChartValue(entry);
248
+ }),
249
+ ]);
210
250
  }
211
- return { columns, rows };
251
+ return { columns, rows, missing };
212
252
  }
@@ -104,7 +104,7 @@ function isCurrencyLike(raw: string, tex: string, following: string): boolean {
104
104
  return closerLooksLikeLaterOpener
105
105
  }
106
106
 
107
- const CELL_LINK_ATTRIBUTES = 'target="_blank" rel="noopener noreferrer" class="text-blue-600 dark:text-blue-400 hover:underline"'
107
+ const CELL_LINK_ATTRIBUTES = 'target="_blank" rel="noopener noreferrer" class="text-[color:var(--mcp-ui-accent,var(--color-blue-600))] dark:text-[color:var(--mcp-ui-accent,var(--color-blue-400))] hover:underline"'
108
108
 
109
109
  function cellLinkExtension(): MarkedExtension {
110
110
  return {
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Missing values — one definition shared by tables, chart data views,
3
+ * degraded projections, chart validation and the chart renderer.
4
+ *
5
+ * @since v6.24.0
6
+ *
7
+ * A missing value is a value the payload says EXISTS but was not observed:
8
+ * `null` in a chart dataset (or as a point's `y`), `null` / `undefined` in a
9
+ * table cell. It is not a zero, and it is not the absence of a row or of a
10
+ * label: every view keeps its slot and marks it.
11
+ *
12
+ * Runtime-free on purpose (no `solid-js`): the pure projections in
13
+ * `chart-data-table.ts` and `degraded-projections.ts`, and `validation.ts`
14
+ * (published as the `/validation` subpath), import it.
15
+ */
16
+
17
+ /** The glyph a table cell shows for a missing value — `renderCellValue`'s historical `-`. */
18
+ export const MISSING_VALUE_MARKER = '-'
19
+
20
+ /**
21
+ * Chart types that encode a value as a share of a whole (or, for polar area,
22
+ * as a wedge whose emptiness reads as zero). Chart.js parses a `null` share
23
+ * as `0`, so these types refuse missing values instead of drawing them.
24
+ */
25
+ export const PART_TO_WHOLE_CHART_TYPES: ReadonlySet<string> = new Set([
26
+ 'pie',
27
+ 'doughnut',
28
+ 'polarArea',
29
+ ])
30
+
31
+ /**
32
+ * Whether a raw TABLE cell value is displayed as {@link MISSING_VALUE_MARKER}.
33
+ *
34
+ * Mirrors the rules `renderCellValue` applies before any formatting — `null`,
35
+ * `undefined`, and a string that is empty or the word `undefined` once the
36
+ * backend's `"X – undefined"` debris is stripped — plus the literal marker
37
+ * itself (whitespace around it aside, which a table cell does not render),
38
+ * since it looks identical on screen and so must read identically.
39
+ *
40
+ * Objects are never missing here: `renderCellValue` renders them (link,
41
+ * name, or escaped JSON).
42
+ */
43
+ export function isMissingCellValue(value: unknown): boolean {
44
+ if (value === null || value === undefined) return true
45
+ if (typeof value === 'object') return false
46
+ const cleaned = stripUndefinedDebris(String(value)).trim()
47
+ return cleaned === '' || cleaned === MISSING_VALUE_MARKER || cleaned.toLowerCase() === 'undefined'
48
+ }
49
+
50
+ /**
51
+ * Removes the `"Text – undefined"` / `"undefined – Text"` debris some
52
+ * backends concatenate. Shared with `renderCellValue` so both apply the very
53
+ * same cleanup.
54
+ */
55
+ export function stripUndefinedDebris(value: string): string {
56
+ return value.replace(/\s*[–-]\s*undefined\s*$/gi, '').replace(/^undefined\s*[–-]\s*/gi, '')
57
+ }
58
+
59
+ /**
60
+ * Whether one CHART data entry is missing: `null` / `undefined`, a non-finite
61
+ * number (Chart.js parses those to `null` too), or a point whose `y` is.
62
+ */
63
+ export function isMissingChartValue(value: unknown): boolean {
64
+ if (value === null || value === undefined) return true
65
+ if (typeof value === 'number') return !Number.isFinite(value)
66
+ if (typeof value === 'object' && !Array.isArray(value)) {
67
+ const y = (value as { y?: unknown }).y
68
+ return y === null || y === undefined || (typeof y === 'number' && !Number.isFinite(y))
69
+ }
70
+ return false
71
+ }
72
+
73
+ /**
74
+ * What a chart's missing values amount to.
75
+ *
76
+ * Only ENTRIES count: an explicit `null` (or non-finite number, or a point
77
+ * whose `y` is one). A dataset with no entry at all (`data: []`) is not
78
+ * "missing" — it is how 6.23.0 and earlier payloads said "nothing yet", e.g.
79
+ * while streaming, and it keeps rendering exactly as it did then.
80
+ */
81
+ export interface ChartMissingSummary {
82
+ /** At least one entry of one dataset is missing. */
83
+ hasMissing: boolean
84
+ /** Per dataset, in order: it has entries and every one of them is missing. */
85
+ fullyMissing: boolean[]
86
+ /**
87
+ * Per dataset, in order: at least one observed value AND at least one
88
+ * missing entry — the series a reader actually sees broken.
89
+ */
90
+ partiallyMissing: boolean[]
91
+ /** Something is missing and no dataset has a single observed value: nothing to plot. */
92
+ allMissing: boolean
93
+ }
94
+
95
+ interface ChartDataLike {
96
+ data?: {
97
+ datasets?: Array<{ data?: unknown[] } | null | undefined>
98
+ }
99
+ }
100
+
101
+ /** Summarises the missing values of a chart payload. Never throws on a malformed payload. */
102
+ export function summarizeChartMissing(params: ChartDataLike | null | undefined): ChartMissingSummary {
103
+ const datasets = Array.isArray(params?.data?.datasets) ? params.data.datasets : []
104
+ const fullyMissing: boolean[] = []
105
+ const partiallyMissing: boolean[] = []
106
+ let anyMissing = false
107
+ let anyObserved = false
108
+ for (const dataset of datasets) {
109
+ const data = Array.isArray(dataset?.data) ? dataset.data : []
110
+ let observed = 0
111
+ let missing = 0
112
+ for (const value of data) {
113
+ if (isMissingChartValue(value)) missing += 1
114
+ else observed += 1
115
+ }
116
+ fullyMissing.push(missing > 0 && observed === 0)
117
+ partiallyMissing.push(observed > 0 && missing > 0)
118
+ anyMissing ||= missing > 0
119
+ anyObserved ||= observed > 0
120
+ }
121
+ return {
122
+ hasMissing: anyMissing,
123
+ fullyMissing,
124
+ partiallyMissing,
125
+ allMissing: anyMissing && !anyObserved,
126
+ }
127
+ }
@@ -0,0 +1,101 @@
1
+ import { createEffect, onCleanup } from 'solid-js'
2
+
3
+ const THEME_PREFIX = '--mcp-ui-'
4
+
5
+ /** Read only explicitly supplied host tokens; blank values retain engine defaults. */
6
+ export function readThemeTokens(
7
+ element: Element | undefined,
8
+ names: readonly string[],
9
+ ): Record<string, string | undefined> {
10
+ const result: Record<string, string | undefined> = {}
11
+ const view = element?.ownerDocument.defaultView
12
+ const style = element && view ? view.getComputedStyle(element) : undefined
13
+ for (const name of names) result[name] = style?.getPropertyValue(name).trim() || undefined
14
+ return result
15
+ }
16
+
17
+ /** Observe inherited theme changes, including moving a mounted renderer into a portal. */
18
+ export function observeTheme(element: HTMLElement, callback: () => void): () => void {
19
+ const view = element.ownerDocument.defaultView
20
+ if (!view?.MutationObserver) return () => {}
21
+ let stopped = false
22
+ let pending = false
23
+ const schedule = () => {
24
+ if (pending || stopped) return
25
+ pending = true
26
+ queueMicrotask(() => {
27
+ pending = false
28
+ if (!stopped) callback()
29
+ })
30
+ }
31
+ const observer = new view.MutationObserver((records) => {
32
+ if (records.some((record) => {
33
+ if (record.type === 'attributes') {
34
+ return record.target instanceof view.Element && record.target.contains(element)
35
+ }
36
+ return [...record.addedNodes, ...record.removedNodes].some(
37
+ (node) => node === element || node.contains(element),
38
+ )
39
+ })) schedule()
40
+ })
41
+ // Attribute names are deliberately not filtered: hosts may use any data-* selector.
42
+ observer.observe(element.ownerDocument.documentElement, {
43
+ attributes: true,
44
+ childList: true,
45
+ subtree: true,
46
+ })
47
+ return () => {
48
+ stopped = true
49
+ observer.disconnect()
50
+ }
51
+ }
52
+
53
+ /**
54
+ * A body portal cannot inherit a local host theme. Copy only our effective
55
+ * custom properties and dark state from its stationary anchor, and keep them
56
+ * synchronized for the lifetime of the Solid owner. Geometry stays unchanged.
57
+ */
58
+ export function bindPortalTheme(portal: HTMLElement, source: () => HTMLElement | undefined): void {
59
+ createEffect(() => {
60
+ const anchor = source()
61
+ const view = anchor?.ownerDocument.defaultView
62
+ if (!anchor || !view) return
63
+ const copied = new Set<string>()
64
+ const sync = () => {
65
+ const style = view.getComputedStyle(anchor)
66
+ const inherited = portal.parentElement ? view.getComputedStyle(portal.parentElement) : undefined
67
+ const names = new Set(copied)
68
+ for (const computed of [style, inherited]) {
69
+ if (!computed) continue
70
+ for (let index = 0; index < computed.length; index++) {
71
+ const name = computed.item(index)
72
+ if (name.startsWith(THEME_PREFIX)) names.add(name)
73
+ }
74
+ }
75
+ const values = new Map<string, string>()
76
+ for (const name of names) {
77
+ const value = style.getPropertyValue(name).trim()
78
+ if (value) values.set(name, value)
79
+ // Empty setProperty removes a declaration. Explicitly block a body
80
+ // token when the local scope reset it to the guaranteed-invalid value.
81
+ else if (inherited?.getPropertyValue(name).trim()) values.set(name, 'initial')
82
+ }
83
+ for (const name of copied) {
84
+ if (!values.has(name)) portal.style.removeProperty(name)
85
+ }
86
+ copied.clear()
87
+ for (const [name, value] of values) {
88
+ if (portal.style.getPropertyValue(name) !== value) portal.style.setProperty(name, value)
89
+ copied.add(name)
90
+ }
91
+ portal.classList.toggle('dark', anchor.closest('.dark') !== null)
92
+ }
93
+ sync()
94
+ const stop = observeTheme(anchor, sync)
95
+ onCleanup(() => {
96
+ stop()
97
+ for (const name of copied) portal.style.removeProperty(name)
98
+ portal.classList.remove('dark')
99
+ })
100
+ })
101
+ }