@seed-ship/mcp-ui-solid 6.22.0 → 6.24.0

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 (120) hide show
  1. package/CHANGELOG.md +219 -0
  2. package/README.md +209 -2
  3. package/dist/components/ChartJSRenderer.cjs +192 -66
  4. package/dist/components/ChartJSRenderer.cjs.map +1 -1
  5. package/dist/components/ChartJSRenderer.d.ts.map +1 -1
  6. package/dist/components/ChartJSRenderer.js +192 -66
  7. package/dist/components/ChartJSRenderer.js.map +1 -1
  8. package/dist/components/DegradedFallback.cjs +34 -2
  9. package/dist/components/DegradedFallback.cjs.map +1 -1
  10. package/dist/components/DegradedFallback.d.ts +7 -0
  11. package/dist/components/DegradedFallback.d.ts.map +1 -1
  12. package/dist/components/DegradedFallback.js +34 -2
  13. package/dist/components/DegradedFallback.js.map +1 -1
  14. package/dist/components/MissingValue.cjs +43 -0
  15. package/dist/components/MissingValue.cjs.map +1 -0
  16. package/dist/components/MissingValue.d.ts +27 -0
  17. package/dist/components/MissingValue.d.ts.map +1 -0
  18. package/dist/components/MissingValue.js +43 -0
  19. package/dist/components/MissingValue.js.map +1 -0
  20. package/dist/components/UIResourceRenderer.cjs +250 -202
  21. package/dist/components/UIResourceRenderer.cjs.map +1 -1
  22. package/dist/components/UIResourceRenderer.d.ts +2 -1
  23. package/dist/components/UIResourceRenderer.d.ts.map +1 -1
  24. package/dist/components/UIResourceRenderer.js +250 -202
  25. package/dist/components/UIResourceRenderer.js.map +1 -1
  26. package/dist/components/chart-data-table.cjs +30 -5
  27. package/dist/components/chart-data-table.cjs.map +1 -1
  28. package/dist/components/chart-data-table.d.ts +18 -1
  29. package/dist/components/chart-data-table.d.ts.map +1 -1
  30. package/dist/components/chart-data-table.js +30 -5
  31. package/dist/components/chart-data-table.js.map +1 -1
  32. package/dist/components/chart-missing-plugin.cjs +194 -0
  33. package/dist/components/chart-missing-plugin.cjs.map +1 -0
  34. package/dist/components/chart-missing-plugin.d.ts +127 -0
  35. package/dist/components/chart-missing-plugin.d.ts.map +1 -0
  36. package/dist/components/chart-missing-plugin.js +194 -0
  37. package/dist/components/chart-missing-plugin.js.map +1 -0
  38. package/dist/context/MCPUIConfigContext.cjs.map +1 -1
  39. package/dist/context/MCPUIConfigContext.d.ts +57 -28
  40. package/dist/context/MCPUIConfigContext.d.ts.map +1 -1
  41. package/dist/context/MCPUIConfigContext.js.map +1 -1
  42. package/dist/context/MCPUIMathContext.cjs +19 -0
  43. package/dist/context/MCPUIMathContext.cjs.map +1 -0
  44. package/dist/context/MCPUIMathContext.d.ts +18 -0
  45. package/dist/context/MCPUIMathContext.d.ts.map +1 -0
  46. package/dist/context/MCPUIMathContext.js +19 -0
  47. package/dist/context/MCPUIMathContext.js.map +1 -0
  48. package/dist/context/MCPUIStringsContext.cjs +12 -1
  49. package/dist/context/MCPUIStringsContext.cjs.map +1 -1
  50. package/dist/context/MCPUIStringsContext.d.ts +62 -0
  51. package/dist/context/MCPUIStringsContext.d.ts.map +1 -1
  52. package/dist/context/MCPUIStringsContext.js +12 -1
  53. package/dist/context/MCPUIStringsContext.js.map +1 -1
  54. package/dist/index.cjs +2 -0
  55. package/dist/index.cjs.map +1 -1
  56. package/dist/index.d.cts +3 -1
  57. package/dist/index.d.ts +3 -1
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +2 -0
  60. package/dist/index.js.map +1 -1
  61. package/dist/plugins/katex.cjs +23 -0
  62. package/dist/plugins/katex.cjs.map +1 -0
  63. package/dist/plugins/katex.d.cts +7 -0
  64. package/dist/plugins/katex.d.ts +7 -0
  65. package/dist/plugins/katex.d.ts.map +1 -0
  66. package/dist/plugins/katex.js +23 -0
  67. package/dist/plugins/katex.js.map +1 -0
  68. package/dist/services/component-registry.cjs +9 -2
  69. package/dist/services/component-registry.cjs.map +1 -1
  70. package/dist/services/component-registry.d.ts.map +1 -1
  71. package/dist/services/component-registry.js +9 -2
  72. package/dist/services/component-registry.js.map +1 -1
  73. package/dist/services/validation.cjs +31 -8
  74. package/dist/services/validation.cjs.map +1 -1
  75. package/dist/services/validation.d.ts.map +1 -1
  76. package/dist/services/validation.js +31 -8
  77. package/dist/services/validation.js.map +1 -1
  78. package/dist/types/index.d.ts +14 -2
  79. package/dist/types/index.d.ts.map +1 -1
  80. package/dist/utils/degraded-projections.cjs +19 -3
  81. package/dist/utils/degraded-projections.cjs.map +1 -1
  82. package/dist/utils/degraded-projections.d.ts +22 -2
  83. package/dist/utils/degraded-projections.d.ts.map +1 -1
  84. package/dist/utils/degraded-projections.js +19 -3
  85. package/dist/utils/degraded-projections.js.map +1 -1
  86. package/dist/utils/iframe-coep.cjs.map +1 -1
  87. package/dist/utils/iframe-coep.d.ts +7 -6
  88. package/dist/utils/iframe-coep.d.ts.map +1 -1
  89. package/dist/utils/iframe-coep.js.map +1 -1
  90. package/dist/utils/math-markdown.cjs +240 -0
  91. package/dist/utils/math-markdown.cjs.map +1 -0
  92. package/dist/utils/math-markdown.d.ts +16 -0
  93. package/dist/utils/math-markdown.d.ts.map +1 -0
  94. package/dist/utils/math-markdown.js +240 -0
  95. package/dist/utils/math-markdown.js.map +1 -0
  96. package/dist/utils/missing-value.cjs +60 -0
  97. package/dist/utils/missing-value.cjs.map +1 -0
  98. package/dist/utils/missing-value.d.ts +79 -0
  99. package/dist/utils/missing-value.d.ts.map +1 -0
  100. package/dist/utils/missing-value.js +60 -0
  101. package/dist/utils/missing-value.js.map +1 -0
  102. package/package.json +23 -4
  103. package/src/components/ChartJSRenderer.tsx +159 -6
  104. package/src/components/DegradedFallback.tsx +24 -2
  105. package/src/components/MissingValue.tsx +61 -0
  106. package/src/components/UIResourceRenderer.tsx +64 -10
  107. package/src/components/chart-data-table.ts +50 -6
  108. package/src/components/chart-missing-plugin.ts +334 -0
  109. package/src/context/MCPUIConfigContext.tsx +57 -28
  110. package/src/context/MCPUIMathContext.tsx +32 -0
  111. package/src/context/MCPUIStringsContext.tsx +76 -0
  112. package/src/index.ts +4 -0
  113. package/src/plugins/katex.ts +25 -0
  114. package/src/services/component-registry.ts +10 -2
  115. package/src/services/validation.ts +44 -9
  116. package/src/types/index.ts +13 -1
  117. package/src/utils/degraded-projections.ts +44 -4
  118. package/src/utils/iframe-coep.ts +7 -6
  119. package/src/utils/math-markdown.ts +222 -0
  120. package/src/utils/missing-value.ts +127 -0
@@ -32,7 +32,17 @@ import type { Accessor, JSX } from 'solid-js';
32
32
  import type { UIComponent, ChartComponentParams } from '../types';
33
33
  import { ExpandableWrapper, useExpanded } from './ExpandableWrapper';
34
34
  import { DegradedFallback } from './DegradedFallback';
35
+ import { MissingValueLegend, MissingValueMark } from './MissingValue';
35
36
  import { chartToDataTable } from './chart-data-table';
37
+ import {
38
+ applyUnitAxisTitle,
39
+ createMissingLegendGenerator,
40
+ createMissingValuesPlugin,
41
+ createTooltipLabel,
42
+ missingNoteKind,
43
+ withDatasetsNoGapSpan,
44
+ } from './chart-missing-plugin';
45
+ import { PART_TO_WHOLE_CHART_TYPES, summarizeChartMissing } from '../utils/missing-value';
36
46
  import { useTelemetry } from '../context/MCPUITelemetryContext';
37
47
  import { useMCPUIConfig } from '../context/MCPUIConfigContext';
38
48
  import { formatMCPUIString, useMCPUIStrings } from '../context/MCPUIStringsContext';
@@ -320,9 +330,51 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
320
330
  point: strings.chartTablePoint,
321
331
  label: strings.degradedColLabel,
322
332
  seriesName: strings.degradedSeries,
333
+ withUnit: strings.chartLabelWithUnit,
323
334
  })
324
335
  );
336
+ // Absence is never zero: which entries are missing, and which series have
337
+ // nothing at all. Drives the notes, the plugin, the legend and the guard.
338
+ const missingSummary = createMemo(() => summarizeChartMissing(params()));
339
+ const unit = () => {
340
+ const value = params().unit;
341
+ return typeof value === 'string' && value !== '' ? value : undefined;
342
+ };
343
+ const anyMissingCell = () => tableData().missing.some((row) => row.some(Boolean));
344
+ // Never for a part-to-whole chart: it refuses missing values outright (the
345
+ // render effect turns it into the data table), so no convention applies —
346
+ // and the async guard must not leave a wrong note in the SSR markup.
347
+ const notesShown = () =>
348
+ missingSummary().hasMissing &&
349
+ !PART_TO_WHOLE_CHART_TYPES.has(params().type) &&
350
+ activeView() === 'chart' &&
351
+ !error();
352
+ const noteKind = () => missingNoteKind(params().type, params().options);
353
+ // The convention line explains a mark the reader can SEE: every missing bar
354
+ // gets a dashed stub, but a line or a scatter only shows an absence inside a
355
+ // series that still has values — a series with no value at all draws
356
+ // nothing, and the "no data" line alone covers it.
357
+ const conventionNoteShown = () =>
358
+ noteKind() === 'bars'
359
+ ? missingSummary().hasMissing
360
+ : missingSummary().partiallyMissing.some(Boolean);
361
+ // The series the chart has no data for, as a locale-aware list.
362
+ const noDataSeries = () => {
363
+ const names = (params().data?.datasets ?? [])
364
+ .map((dataset, index) =>
365
+ missingSummary().fullyMissing[index]
366
+ ? dataset?.label || formatMCPUIString(strings.degradedSeries, { n: index + 1 })
367
+ : null
368
+ )
369
+ .filter((name): name is string => name !== null);
370
+ try {
371
+ return new Intl.ListFormat(strings.locale, { type: 'conjunction' }).format(names);
372
+ } catch {
373
+ return names.join(', ');
374
+ }
375
+ };
325
376
  const descriptionId = createUniqueId();
377
+ const notesId = createUniqueId();
326
378
  const title = () => params().title || strings.chartView;
327
379
 
328
380
  // v6.1.0 — export visibility :
@@ -456,6 +508,8 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
456
508
  // plugin options at construction and the plugin only hooks charts created
457
509
  // after it was registered).
458
510
  const chartParams = params();
511
+ const chartMissing = missingSummary();
512
+ const chartUnit = unit();
459
513
  const zoom = zoomOptions();
460
514
  const version = ++renderVersion;
461
515
 
@@ -474,6 +528,14 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
474
528
  chartInstance = null;
475
529
  }
476
530
 
531
+ // A share of a whole cannot be missing: Chart.js parses `null` as 0 and
532
+ // would draw it as a zero slice. Validation rejects this earlier; this
533
+ // is the guard for a host that bypassed it. Thrown here so the catch
534
+ // below shows the data table (with the missing cells marked).
535
+ if (chartMissing.hasMissing && PART_TO_WHOLE_CHART_TYPES.has(chartParams.type)) {
536
+ throw new Error(strings.chartMissingUnsupported);
537
+ }
538
+
477
539
  // Build options, merging time-axis config if present (v3.1.0)
478
540
  const hostZoomOptions = chartParams.options?.plugins?.zoom;
479
541
  const baseOptions: any = {
@@ -517,12 +579,69 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
517
579
  };
518
580
  }
519
581
 
520
- // Create new chart
521
- chartInstance = new Chart(canvasRef, {
582
+ // Unit (v6.24.0): value-axis title and tooltip suffix. Both are no-ops
583
+ // on a payload without a unit.
584
+ applyUnitAxisTitle(baseOptions, chartParams.type, chartUnit);
585
+
586
+ const chartConfig: any = {
522
587
  type: chartParams.type,
523
588
  data: chartParams.data,
524
589
  options: baseOptions,
525
- });
590
+ };
591
+
592
+ if (chartUnit !== undefined || chartMissing.hasMissing) {
593
+ const tooltip = baseOptions.plugins.tooltip;
594
+ // `tooltip: false` (or `null`) is Chart.js's per-chart plugin opt-out:
595
+ // the payload turned tooltips off, and adding a callback object here
596
+ // would turn them back on. A host-supplied label callback is the
597
+ // host's word too. Both are left exactly as written.
598
+ if (tooltip !== false && tooltip !== null && !tooltip?.callbacks?.label) {
599
+ baseOptions.plugins.tooltip = {
600
+ ...tooltip,
601
+ callbacks: {
602
+ ...tooltip?.callbacks,
603
+ label: createTooltipLabel(() => strings, chartUnit, chartParams.type),
604
+ },
605
+ };
606
+ }
607
+ }
608
+
609
+ if (chartMissing.hasMissing) {
610
+ // A line must break at a missing value, not join its neighbours.
611
+ chartConfig.data = withDatasetsNoGapSpan(chartParams.data);
612
+
613
+ // Legend of a fully-missing series: "{series} (no data)". Dataset
614
+ // legends only — pie-like legends list categories, not series.
615
+ const defaultGenerate =
616
+ baseOptions.plugins.legend?.labels?.generateLabels ??
617
+ Chart?.defaults?.plugins?.legend?.labels?.generateLabels;
618
+ if (typeof defaultGenerate === 'function') {
619
+ baseOptions.plugins.legend = {
620
+ ...baseOptions.plugins.legend,
621
+ labels: {
622
+ ...baseOptions.plugins.legend?.labels,
623
+ generateLabels: createMissingLegendGenerator(
624
+ defaultGenerate,
625
+ chartMissing.fullyMissing,
626
+ () => strings
627
+ ),
628
+ },
629
+ };
630
+ }
631
+
632
+ // Per-chart plugin (never `Chart.register`): the dashed stub of a
633
+ // missing bar, and "no data" on a chart with nothing to plot.
634
+ chartConfig.plugins = [
635
+ createMissingValuesPlugin({
636
+ allMissing: chartMissing.allMissing,
637
+ getNoDataText: () => strings.chartNoData,
638
+ getFontFamily: () => Chart?.defaults?.font?.family,
639
+ }),
640
+ ];
641
+ }
642
+
643
+ // Create new chart
644
+ chartInstance = new Chart(canvasRef, chartConfig);
526
645
 
527
646
  // A freshly built chart always sits on its initial viewport, so the
528
647
  // reset control goes away until the user zooms again.
@@ -761,12 +880,17 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
761
880
  </thead>
762
881
  <tbody>
763
882
  <For each={tableData().rows}>
764
- {(row) => (
883
+ {(row, rowIndex) => (
765
884
  <tr class="border-t border-gray-100 dark:border-gray-700">
766
885
  <For each={tableData().columns}>
767
886
  {(_column, index) => (
768
887
  <td class="px-3 py-2 text-gray-700 dark:text-gray-300">
769
- {String(row[index()] ?? '')}
888
+ <Show
889
+ when={tableData().missing[rowIndex()]?.[index()]}
890
+ fallback={String(row[index()] ?? '')}
891
+ >
892
+ <MissingValueMark />
893
+ </Show>
770
894
  </td>
771
895
  )}
772
896
  </For>
@@ -786,6 +910,9 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
786
910
  </tbody>
787
911
  </table>
788
912
  </div>
913
+ <Show when={anyMissingCell()}>
914
+ <MissingValueLegend class="mt-2" />
915
+ </Show>
789
916
  </Show>
790
917
 
791
918
  <div
@@ -802,7 +929,7 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
802
929
  ref={canvasRef}
803
930
  role="img"
804
931
  aria-label={title()}
805
- aria-describedby={descriptionId}
932
+ aria-describedby={notesShown() ? `${descriptionId} ${notesId}` : descriptionId}
806
933
  aria-hidden={activeView() !== 'chart' || Boolean(error())}
807
934
  />
808
935
  <Show when={isLoading() && activeView() === 'chart'}>
@@ -814,6 +941,32 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
814
941
  </div>
815
942
  </Show>
816
943
  </div>
944
+
945
+ {/* Missing values (v6.24.0): how THIS chart type shows them, and which
946
+ series have no data at all. Linked from the canvas, so a screen
947
+ reader hears the convention with the chart. */}
948
+ <Show when={notesShown()}>
949
+ <div
950
+ data-mcp-chart-notes=""
951
+ id={notesId}
952
+ class="mt-2 space-y-0.5 text-xs text-gray-600 dark:text-gray-400"
953
+ >
954
+ <Show when={conventionNoteShown()}>
955
+ <p data-mcp-chart-note={noteKind()}>
956
+ {noteKind() === 'bars'
957
+ ? strings.chartMissingBars
958
+ : noteKind() === 'omitted'
959
+ ? strings.chartMissingPoints
960
+ : strings.chartMissingGaps}
961
+ </p>
962
+ </Show>
963
+ <Show when={missingSummary().fullyMissing.some(Boolean)}>
964
+ <p data-mcp-chart-note="no-data">
965
+ {formatMCPUIString(strings.chartSeriesNoData, { series: noDataSeries() })}
966
+ </p>
967
+ </Show>
968
+ </div>
969
+ </Show>
817
970
  </div>
818
971
  )}
819
972
  />
@@ -17,6 +17,7 @@
17
17
 
18
18
  import { Component, For, Show } from 'solid-js';
19
19
  import { formatMCPUIString, useMCPUIStrings } from '../context/MCPUIStringsContext';
20
+ import { MissingValueLegend, MissingValueMark } from './MissingValue';
20
21
 
21
22
  export interface DegradedFallbackProps {
22
23
  /** Short, human-readable reason the native render was skipped/failed. */
@@ -35,6 +36,13 @@ export interface DegradedFallbackProps {
35
36
  caption?: string;
36
37
  /** Max rows to render before truncating (default 50). */
37
38
  maxRows?: number;
39
+ /**
40
+ * Same shape as `rows`: `true` marks a cell whose value is missing, which
41
+ * renders as `<MissingValueMark/>` instead of its text. A
42
+ * `<MissingValueLegend/>` follows the table when a SHOWN cell is missing.
43
+ * Omitted, the output is unchanged. @since v6.24.0
44
+ */
45
+ missing?: boolean[][];
38
46
  }
39
47
 
40
48
  export const DegradedFallback: Component<DegradedFallbackProps> = (props) => {
@@ -43,6 +51,15 @@ export const DegradedFallback: Component<DegradedFallbackProps> = (props) => {
43
51
  const allRows = () => props.rows ?? [];
44
52
  const shownRows = () => allRows().slice(0, maxRows());
45
53
  const hiddenCount = () => Math.max(0, allRows().length - shownRows().length);
54
+ const isMissing = (rowIndex: number, colIndex: number) =>
55
+ props.missing?.[rowIndex]?.[colIndex] === true;
56
+ const anyShownMissing = () => {
57
+ const columnCount = props.columns?.length ?? 0;
58
+ return shownRows().some((_row, r) => {
59
+ for (let c = 0; c < columnCount; c += 1) if (isMissing(r, c)) return true;
60
+ return false;
61
+ });
62
+ };
46
63
  const hasTable = () => (props.columns?.length ?? 0) > 0 && allRows().length > 0;
47
64
 
48
65
  return (
@@ -69,12 +86,14 @@ export const DegradedFallback: Component<DegradedFallbackProps> = (props) => {
69
86
  </thead>
70
87
  <tbody>
71
88
  <For each={shownRows()}>
72
- {(row) => (
89
+ {(row, rowIndex) => (
73
90
  <tr class="border-t border-amber-100 dark:border-amber-800/60">
74
91
  <For each={props.columns}>
75
92
  {(_col, i) => (
76
93
  <td class="px-2 py-1 text-amber-800 dark:text-amber-200">
77
- {String(row[i()] ?? '')}
94
+ <Show when={isMissing(rowIndex(), i())} fallback={String(row[i()] ?? '')}>
95
+ <MissingValueMark />
96
+ </Show>
78
97
  </td>
79
98
  )}
80
99
  </For>
@@ -84,6 +103,9 @@ export const DegradedFallback: Component<DegradedFallbackProps> = (props) => {
84
103
  </tbody>
85
104
  </table>
86
105
  </div>
106
+ <Show when={anyShownMissing()}>
107
+ <MissingValueLegend class="mt-2" />
108
+ </Show>
87
109
  <Show when={hiddenCount() > 0}>
88
110
  <p class="mt-1 text-[10px] text-amber-600 dark:text-amber-400">
89
111
  {formatMCPUIString(strings.degradedMoreRows, { count: hiddenCount() })}
@@ -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-gray-400 dark:text-gray-500">
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-gray-500 dark:text-gray-400 ${props.class ?? ''}`}
57
+ >
58
+ {formatMCPUIString(strings.missingValueLegend, { marker: MISSING_VALUE_MARKER })}
59
+ </p>
60
+ )
61
+ }
@@ -28,6 +28,8 @@ import {
28
28
  useMCPUIStrings,
29
29
  } from '../context/MCPUIStringsContext'
30
30
  import { useMCPUIConfig } from '../context/MCPUIConfigContext'
31
+ import { isMissingCellValue, stripUndefinedDebris } from '../utils/missing-value'
32
+ import { MissingValueLegend, MissingValueMark } from './MissingValue'
31
33
  import { shouldSetCredentialless } from '../utils/iframe-coep'
32
34
  import { IframeFallbackLink } from './IframeFallbackLink'
33
35
 
@@ -67,6 +69,8 @@ import { RenderProvider } from './RenderContext'
67
69
  import { useAction } from '../hooks/useAction'
68
70
  import { createResponsiveGrid } from '../hooks/createResponsiveGrid'
69
71
  import { marked } from 'marked'
72
+ import { useMCPUIMath, type MathRenderer } from '../context/MCPUIMathContext'
73
+ import { parseMathMarkdown } from '../utils/math-markdown'
70
74
 
71
75
  /**
72
76
  * Copy button component with visual feedback
@@ -345,6 +349,7 @@ function ChartRenderer(props: {
345
349
  caption={strings.chartQuickchartCaption}
346
350
  {...chartToDegradedTable(params() ?? {}, {
347
351
  series: strings.degradedSeries,
352
+ withUnit: strings.chartLabelWithUnit,
348
353
  })}
349
354
  />
350
355
  </div>
@@ -443,6 +448,9 @@ export function highlightQuery(html: string, query: string): string {
443
448
  const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_TEXT)
444
449
  const textNodes: Text[] = []
445
450
  for (let node = walker.nextNode(); node; node = walker.nextNode()) {
451
+ // A HTML <mark> inside MathML can change or break its layout/semantics.
452
+ // Keep formulas intact; ordinary text around them remains searchable.
453
+ if (node.parentElement?.closest('math')) continue
446
454
  textNodes.push(node as Text)
447
455
  }
448
456
 
@@ -595,7 +603,7 @@ function transformCellCitations(text: string, ctx: CitationCtx): string {
595
603
  )
596
604
  }
597
605
 
598
- export function renderCellValue(value: any, citationCtx?: CitationCtx): string {
606
+ export function renderCellValue(value: any, citationCtx?: CitationCtx, mathRenderer?: MathRenderer): string {
599
607
  // Handle null/undefined
600
608
  if (value === null || value === undefined) {
601
609
  return '-'
@@ -634,9 +642,10 @@ export function renderCellValue(value: any, citationCtx?: CitationCtx): string {
634
642
 
635
643
  // Clean up "undefined" patterns from backend data
636
644
  // Pattern 1: "Text – undefined" or "Text - undefined" → "Text"
637
- strValue = strValue.replace(/\s*[–-]\s*undefined\s*$/gi, '')
638
645
  // Pattern 2: "undefined – Text" or "undefined - Text" → "Text"
639
- strValue = strValue.replace(/^undefined\s*[–-]\s*/gi, '')
646
+ // (shared with `isMissingCellValue`, so the cells shown as `-` and the cells
647
+ // the table marks and explains as missing can never drift apart)
648
+ strValue = stripUndefinedDebris(strValue)
640
649
  // Pattern 3: standalone "undefined" → "-"
641
650
  if (strValue.trim().toLowerCase() === 'undefined') {
642
651
  return '-'
@@ -646,6 +655,18 @@ export function renderCellValue(value: any, citationCtx?: CitationCtx): string {
646
655
  return '-'
647
656
  }
648
657
 
658
+ // Math must be tokenized before Markdown or citation rewrites can consume
659
+ // TeX underscores or bracket arguments (e.g. \sqrt[3]{x}). The opt-in path
660
+ // transforms citations only in ordinary text tokens, never math/code/URLs.
661
+ if (mathRenderer && strValue.includes('$')) {
662
+ return parseMathMarkdown(
663
+ strValue,
664
+ mathRenderer,
665
+ 'cellMarkdown',
666
+ citationCtx ? (text) => transformCellCitations(text, citationCtx) : undefined,
667
+ )
668
+ }
669
+
649
670
  // Detect and convert markdown links: [text](url) — runs FIRST because
650
671
  // the citation transform's negative lookahead `(?!\()` would also skip
651
672
  // these, but handling them here keeps the existing return path.
@@ -706,6 +727,7 @@ function TableRenderer(props: {
706
727
  const tableParams = props.component.params as any
707
728
  let scrollContainerRef: HTMLDivElement | undefined
708
729
  const strings = useMCPUIStrings()
730
+ const mathRenderer = useMCPUIMath()
709
731
 
710
732
  // v5.7.0 — opt-in citation chip rendering inside cells. When `citationMap`
711
733
  // is present in params, build a CitationCtx once and thread it through
@@ -748,12 +770,18 @@ function TableRenderer(props: {
748
770
  const dir = sortDir()
749
771
  if (!key || !dir) return r
750
772
  const col = columns().find((c: any) => c.key === key)
751
- const isNum = col?.type === 'number' || (r.length > 0 && typeof r[0]?.[key] === 'number')
773
+ // v6.24.0 — numeric mode is decided from the first NON-missing value, so a
774
+ // missing first row no longer flips a numeric column to string order.
775
+ const firstObserved = r.find((row: any) => !isMissingCellValue(row?.[key]))?.[key]
776
+ const isNum = col?.type === 'number' || typeof firstObserved === 'number'
752
777
  return [...r].sort((a: any, b: any) => {
753
778
  const va = a[key], vb = b[key]
754
- if (va == null && vb == null) return 0
755
- if (va == null) return 1
756
- if (vb == null) return -1
779
+ // Missing values (null, undefined, '', 'undefined', '-') sort last in
780
+ // both directions — never as 0 and never first.
781
+ const missA = isMissingCellValue(va), missB = isMissingCellValue(vb)
782
+ if (missA && missB) return 0
783
+ if (missA) return 1
784
+ if (missB) return -1
757
785
  let cmp: number
758
786
  if (isNum) {
759
787
  cmp = (Number(va) || 0) - (Number(vb) || 0)
@@ -802,7 +830,7 @@ function TableRenderer(props: {
802
830
  return sortedRows().filter((row: any) =>
803
831
  cols.some((col: any) => {
804
832
  const val = row[col.key]
805
- if (val == null) return false
833
+ if (isMissingCellValue(val)) return false
806
834
  return normalize(String(val)).includes(q)
807
835
  })
808
836
  )
@@ -997,6 +1025,14 @@ function TableRenderer(props: {
997
1025
 
998
1026
  const tableId = `table-${Math.random().toString(36).slice(2, 9)}`
999
1027
 
1028
+ // v6.24.0 — whether ANY declared cell of ANY row is missing. Deliberately
1029
+ // computed on all rows (not the current page / filter) so the legend does not
1030
+ // flicker while paging, sorting or searching.
1031
+ const hasMissingCells = createMemo(() => {
1032
+ const cols = columns()
1033
+ return allRows().some((row: any) => cols.some((col: any) => isMissingCellValue(row?.[col.key])))
1034
+ })
1035
+
1000
1036
  // Standard table body (non-virtualized) — uses client pagination when active
1001
1037
  const StandardTableBody = () => (
1002
1038
  <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
@@ -1006,7 +1042,12 @@ function TableRenderer(props: {
1006
1042
  <For each={tableParams.columns}>
1007
1043
  {(column: any) => (
1008
1044
  <td class="px-6 py-4 text-sm text-gray-700 dark:text-gray-200 whitespace-normal break-words leading-relaxed first:pl-6 last:pr-6">
1009
- <SafeHtml html={() => highlightQuery(renderCellValue(row[column.key], citationCtx), debouncedQuery())} />
1045
+ <Show
1046
+ when={!isMissingCellValue(row[column.key])}
1047
+ fallback={<MissingValueMark />}
1048
+ >
1049
+ <SafeHtml html={() => highlightQuery(renderCellValue(row[column.key], citationCtx, mathRenderer()), debouncedQuery())} />
1050
+ </Show>
1010
1051
  </td>
1011
1052
  )}
1012
1053
  </For>
@@ -1045,7 +1086,12 @@ function TableRenderer(props: {
1045
1086
  <For each={tableParams.columns}>
1046
1087
  {(column: any) => (
1047
1088
  <td class="px-6 py-4 text-sm text-gray-700 dark:text-gray-200 whitespace-normal break-words leading-relaxed first:pl-6 last:pr-6">
1048
- <SafeHtml html={() => highlightQuery(renderCellValue(row[column.key], citationCtx), debouncedQuery())} />
1089
+ <Show
1090
+ when={!isMissingCellValue(row[column.key])}
1091
+ fallback={<MissingValueMark />}
1092
+ >
1093
+ <SafeHtml html={() => highlightQuery(renderCellValue(row[column.key], citationCtx, mathRenderer()), debouncedQuery())} />
1094
+ </Show>
1049
1095
  </td>
1050
1096
  )}
1051
1097
  </For>
@@ -1204,6 +1250,11 @@ function TableRenderer(props: {
1204
1250
  </table>
1205
1251
  </div>
1206
1252
 
1253
+ {/* Missing-value legend (v6.24.0) — outside the scroll region, before pagination */}
1254
+ <Show when={hasMissingCells()}>
1255
+ <MissingValueLegend class="mt-2 px-1" />
1256
+ </Show>
1257
+
1207
1258
  {/* Server-side pagination (legacy) */}
1208
1259
  <Show when={tableParams.pagination}>
1209
1260
  <div class="mt-3 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
@@ -1362,6 +1413,7 @@ function extractImageFromMarkdown(
1362
1413
  */
1363
1414
  function TextRenderer(props: { component: UIComponent }) {
1364
1415
  const strings = useMCPUIStrings()
1416
+ const mathRenderer = useMCPUIMath()
1365
1417
  const textParams = props.component.params as any
1366
1418
 
1367
1419
  // Check if this is an image markdown that should be rendered as image
@@ -1399,6 +1451,8 @@ function TextRenderer(props: { component: UIComponent }) {
1399
1451
  return escapeHtml(String(raw))
1400
1452
  }
1401
1453
  if (textParams.markdown && !imageData()) {
1454
+ const renderMath = mathRenderer()
1455
+ if (renderMath) return parseMathMarkdown(String(raw), renderMath, 'prose')
1402
1456
  return sanitizeHtml(marked.parse(raw, { async: false }) as string, SANITIZE_PROFILES.prose)
1403
1457
  }
1404
1458
  return sanitizeHtml(String(raw), SANITIZE_PROFILES.prose)
@@ -1,4 +1,5 @@
1
1
  import { formatMCPUIString } from '../utils/format-string';
2
+ import { isMissingChartValue } from '../utils/missing-value';
2
3
 
3
4
  /**
4
5
  * Column headers and series names of the accessible chart data table.
@@ -26,10 +27,17 @@ export interface ChartDataTableLabels {
26
27
  x: string;
27
28
  y: string;
28
29
  r: string;
30
+ /**
31
+ * A value column header followed by the chart's `unit`. Template — `{label}`,
32
+ * `{unit}`. Only applied when the chart has a unit. Optional so that a
33
+ * complete labels table written for 6.20.0–6.23.0 still type-checks.
34
+ * @since v6.24.0
35
+ */
36
+ withUnit?: string;
29
37
  }
30
38
 
31
39
  /** English defaults for {@link ChartDataTableLabels}. */
32
- export const CHART_DATA_TABLE_LABELS: ChartDataTableLabels = {
40
+ export const CHART_DATA_TABLE_LABELS: Required<ChartDataTableLabels> = {
33
41
  series: 'Series',
34
42
  point: 'Point',
35
43
  label: 'Label',
@@ -37,11 +45,20 @@ export const CHART_DATA_TABLE_LABELS: ChartDataTableLabels = {
37
45
  x: 'x',
38
46
  y: 'y',
39
47
  r: 'r',
48
+ withUnit: '{label} ({unit})',
40
49
  };
41
50
 
42
51
  export interface ChartDataTable {
43
52
  columns: string[];
44
53
  rows: Array<Array<string | number>>;
54
+ /**
55
+ * Same shape as `rows`: `true` for a VALUE cell whose source entry is
56
+ * missing (`null` / non-finite, or a point whose `y` is). A slot past the
57
+ * end of a shorter or empty dataset has no entry and is not flagged. Label,
58
+ * series, point-index, `x` and `r` cells are never missing. `rows` itself
59
+ * still holds `''` there. @since v6.24.0
60
+ */
61
+ missing: boolean[][];
45
62
  }
46
63
 
47
64
  interface ChartDatasetLike {
@@ -51,6 +68,8 @@ interface ChartDatasetLike {
51
68
 
52
69
  export interface ChartDataLike {
53
70
  type?: string;
71
+ /** Unit of the values: appended to the value column headers. @since v6.24.0 */
72
+ unit?: string;
54
73
  data?: {
55
74
  labels?: Array<string | number>;
56
75
  datasets?: ChartDatasetLike[];
@@ -87,6 +106,10 @@ export function chartToDataTable(
87
106
  labels?: Partial<ChartDataTableLabels>
88
107
  ): ChartDataTable {
89
108
  const l: ChartDataTableLabels = { ...CHART_DATA_TABLE_LABELS, ...labels };
109
+ const unit = typeof params.unit === 'string' && params.unit !== '' ? params.unit : undefined;
110
+ const unitTemplate = l.withUnit ?? CHART_DATA_TABLE_LABELS.withUnit;
111
+ const withUnit = (label: string) =>
112
+ unit === undefined ? label : formatMCPUIString(unitTemplate, { label, unit });
90
113
  const datasets = params.data?.datasets ?? [];
91
114
  const dataLabels = params.data?.labels ?? [];
92
115
  const pointData =
@@ -104,10 +127,11 @@ export function chartToDataTable(
104
127
  l.point,
105
128
  ...(hasLabels ? [l.label] : []),
106
129
  l.x,
107
- l.y,
130
+ withUnit(l.y),
108
131
  ...(hasRadius ? [l.r] : []),
109
132
  ];
110
133
  const rows: Array<Array<string | number>> = [];
134
+ const missing: boolean[][] = [];
111
135
 
112
136
  datasets.forEach((dataset, datasetIndex) => {
113
137
  const series = dataset.label || formatMCPUIString(l.seriesName, { n: datasetIndex + 1 });
@@ -121,10 +145,20 @@ export function chartToDataTable(
121
145
  tableCell(point.y),
122
146
  ...(hasRadius ? [tableCell(point.r)] : []),
123
147
  ]);
148
+ // Only the y cell can be missing; a bare entry is its own y.
149
+ const yMissing = isMissingChartValue(isPoint(value) ? point.y : value);
150
+ missing.push([
151
+ false,
152
+ false,
153
+ ...(hasLabels ? [false] : []),
154
+ false,
155
+ yMissing,
156
+ ...(hasRadius ? [false] : []),
157
+ ]);
124
158
  });
125
159
  });
126
160
 
127
- return { columns, rows };
161
+ return { columns, rows, missing };
128
162
  }
129
163
 
130
164
  const rowCount = Math.max(
@@ -134,18 +168,28 @@ export function chartToDataTable(
134
168
  );
135
169
  const columns = [
136
170
  l.label,
137
- ...datasets.map(
138
- (dataset, index) => dataset.label || formatMCPUIString(l.seriesName, { n: index + 1 })
171
+ ...datasets.map((dataset, index) =>
172
+ withUnit(dataset.label || formatMCPUIString(l.seriesName, { n: index + 1 }))
139
173
  ),
140
174
  ];
141
175
  const rows: Array<Array<string | number>> = [];
176
+ const missing: boolean[][] = [];
142
177
 
143
178
  for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) {
144
179
  rows.push([
145
180
  tableCell(dataLabels[rowIndex] ?? rowIndex + 1),
146
181
  ...datasets.map((dataset) => tableCell(dataset.data?.[rowIndex])),
147
182
  ]);
183
+ // Only an ENTRY can be missing: a slot past the end of a shorter (or
184
+ // empty) dataset has no entry, and renders as before (an empty cell).
185
+ missing.push([
186
+ false,
187
+ ...datasets.map(
188
+ (dataset) =>
189
+ rowIndex < (dataset.data?.length ?? 0) && isMissingChartValue(dataset.data?.[rowIndex])
190
+ ),
191
+ ]);
148
192
  }
149
193
 
150
- return { columns, rows };
194
+ return { columns, rows, missing };
151
195
  }