@seed-ship/mcp-ui-solid 6.23.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 (79) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/README.md +107 -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 +237 -199
  21. package/dist/components/UIResourceRenderer.cjs.map +1 -1
  22. package/dist/components/UIResourceRenderer.d.ts.map +1 -1
  23. package/dist/components/UIResourceRenderer.js +237 -199
  24. package/dist/components/UIResourceRenderer.js.map +1 -1
  25. package/dist/components/chart-data-table.cjs +30 -5
  26. package/dist/components/chart-data-table.cjs.map +1 -1
  27. package/dist/components/chart-data-table.d.ts +18 -1
  28. package/dist/components/chart-data-table.d.ts.map +1 -1
  29. package/dist/components/chart-data-table.js +30 -5
  30. package/dist/components/chart-data-table.js.map +1 -1
  31. package/dist/components/chart-missing-plugin.cjs +194 -0
  32. package/dist/components/chart-missing-plugin.cjs.map +1 -0
  33. package/dist/components/chart-missing-plugin.d.ts +127 -0
  34. package/dist/components/chart-missing-plugin.d.ts.map +1 -0
  35. package/dist/components/chart-missing-plugin.js +194 -0
  36. package/dist/components/chart-missing-plugin.js.map +1 -0
  37. package/dist/context/MCPUIStringsContext.cjs +12 -1
  38. package/dist/context/MCPUIStringsContext.cjs.map +1 -1
  39. package/dist/context/MCPUIStringsContext.d.ts +62 -0
  40. package/dist/context/MCPUIStringsContext.d.ts.map +1 -1
  41. package/dist/context/MCPUIStringsContext.js +12 -1
  42. package/dist/context/MCPUIStringsContext.js.map +1 -1
  43. package/dist/services/component-registry.cjs +9 -2
  44. package/dist/services/component-registry.cjs.map +1 -1
  45. package/dist/services/component-registry.d.ts.map +1 -1
  46. package/dist/services/component-registry.js +9 -2
  47. package/dist/services/component-registry.js.map +1 -1
  48. package/dist/services/validation.cjs +31 -8
  49. package/dist/services/validation.cjs.map +1 -1
  50. package/dist/services/validation.d.ts.map +1 -1
  51. package/dist/services/validation.js +31 -8
  52. package/dist/services/validation.js.map +1 -1
  53. package/dist/types/index.d.ts +14 -2
  54. package/dist/types/index.d.ts.map +1 -1
  55. package/dist/utils/degraded-projections.cjs +19 -3
  56. package/dist/utils/degraded-projections.cjs.map +1 -1
  57. package/dist/utils/degraded-projections.d.ts +22 -2
  58. package/dist/utils/degraded-projections.d.ts.map +1 -1
  59. package/dist/utils/degraded-projections.js +19 -3
  60. package/dist/utils/degraded-projections.js.map +1 -1
  61. package/dist/utils/missing-value.cjs +60 -0
  62. package/dist/utils/missing-value.cjs.map +1 -0
  63. package/dist/utils/missing-value.d.ts +79 -0
  64. package/dist/utils/missing-value.d.ts.map +1 -0
  65. package/dist/utils/missing-value.js +60 -0
  66. package/dist/utils/missing-value.js.map +1 -0
  67. package/package.json +3 -3
  68. package/src/components/ChartJSRenderer.tsx +159 -6
  69. package/src/components/DegradedFallback.tsx +24 -2
  70. package/src/components/MissingValue.tsx +61 -0
  71. package/src/components/UIResourceRenderer.tsx +42 -9
  72. package/src/components/chart-data-table.ts +50 -6
  73. package/src/components/chart-missing-plugin.ts +334 -0
  74. package/src/context/MCPUIStringsContext.tsx +76 -0
  75. package/src/services/component-registry.ts +10 -2
  76. package/src/services/validation.ts +44 -9
  77. package/src/types/index.ts +13 -1
  78. package/src/utils/degraded-projections.ts +44 -4
  79. package/src/utils/missing-value.ts +127 -0
@@ -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
 
@@ -347,6 +349,7 @@ function ChartRenderer(props: {
347
349
  caption={strings.chartQuickchartCaption}
348
350
  {...chartToDegradedTable(params() ?? {}, {
349
351
  series: strings.degradedSeries,
352
+ withUnit: strings.chartLabelWithUnit,
350
353
  })}
351
354
  />
352
355
  </div>
@@ -639,9 +642,10 @@ export function renderCellValue(value: any, citationCtx?: CitationCtx, mathRende
639
642
 
640
643
  // Clean up "undefined" patterns from backend data
641
644
  // Pattern 1: "Text – undefined" or "Text - undefined" → "Text"
642
- strValue = strValue.replace(/\s*[–-]\s*undefined\s*$/gi, '')
643
645
  // Pattern 2: "undefined – Text" or "undefined - Text" → "Text"
644
- 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)
645
649
  // Pattern 3: standalone "undefined" → "-"
646
650
  if (strValue.trim().toLowerCase() === 'undefined') {
647
651
  return '-'
@@ -766,12 +770,18 @@ function TableRenderer(props: {
766
770
  const dir = sortDir()
767
771
  if (!key || !dir) return r
768
772
  const col = columns().find((c: any) => c.key === key)
769
- 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'
770
777
  return [...r].sort((a: any, b: any) => {
771
778
  const va = a[key], vb = b[key]
772
- if (va == null && vb == null) return 0
773
- if (va == null) return 1
774
- 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
775
785
  let cmp: number
776
786
  if (isNum) {
777
787
  cmp = (Number(va) || 0) - (Number(vb) || 0)
@@ -820,7 +830,7 @@ function TableRenderer(props: {
820
830
  return sortedRows().filter((row: any) =>
821
831
  cols.some((col: any) => {
822
832
  const val = row[col.key]
823
- if (val == null) return false
833
+ if (isMissingCellValue(val)) return false
824
834
  return normalize(String(val)).includes(q)
825
835
  })
826
836
  )
@@ -1015,6 +1025,14 @@ function TableRenderer(props: {
1015
1025
 
1016
1026
  const tableId = `table-${Math.random().toString(36).slice(2, 9)}`
1017
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
+
1018
1036
  // Standard table body (non-virtualized) — uses client pagination when active
1019
1037
  const StandardTableBody = () => (
1020
1038
  <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
@@ -1024,7 +1042,12 @@ function TableRenderer(props: {
1024
1042
  <For each={tableParams.columns}>
1025
1043
  {(column: any) => (
1026
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">
1027
- <SafeHtml html={() => highlightQuery(renderCellValue(row[column.key], citationCtx, mathRenderer()), 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>
1028
1051
  </td>
1029
1052
  )}
1030
1053
  </For>
@@ -1063,7 +1086,12 @@ function TableRenderer(props: {
1063
1086
  <For each={tableParams.columns}>
1064
1087
  {(column: any) => (
1065
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">
1066
- <SafeHtml html={() => highlightQuery(renderCellValue(row[column.key], citationCtx, mathRenderer()), 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>
1067
1095
  </td>
1068
1096
  )}
1069
1097
  </For>
@@ -1222,6 +1250,11 @@ function TableRenderer(props: {
1222
1250
  </table>
1223
1251
  </div>
1224
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
+
1225
1258
  {/* Server-side pagination (legacy) */}
1226
1259
  <Show when={tableParams.pagination}>
1227
1260
  <div class="mt-3 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
@@ -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
  }