@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.
- package/CHANGELOG.md +219 -0
- package/README.md +209 -2
- package/dist/components/ChartJSRenderer.cjs +192 -66
- package/dist/components/ChartJSRenderer.cjs.map +1 -1
- package/dist/components/ChartJSRenderer.d.ts.map +1 -1
- package/dist/components/ChartJSRenderer.js +192 -66
- package/dist/components/ChartJSRenderer.js.map +1 -1
- package/dist/components/DegradedFallback.cjs +34 -2
- package/dist/components/DegradedFallback.cjs.map +1 -1
- package/dist/components/DegradedFallback.d.ts +7 -0
- package/dist/components/DegradedFallback.d.ts.map +1 -1
- package/dist/components/DegradedFallback.js +34 -2
- package/dist/components/DegradedFallback.js.map +1 -1
- package/dist/components/MissingValue.cjs +43 -0
- package/dist/components/MissingValue.cjs.map +1 -0
- package/dist/components/MissingValue.d.ts +27 -0
- package/dist/components/MissingValue.d.ts.map +1 -0
- package/dist/components/MissingValue.js +43 -0
- package/dist/components/MissingValue.js.map +1 -0
- package/dist/components/UIResourceRenderer.cjs +250 -202
- package/dist/components/UIResourceRenderer.cjs.map +1 -1
- package/dist/components/UIResourceRenderer.d.ts +2 -1
- package/dist/components/UIResourceRenderer.d.ts.map +1 -1
- package/dist/components/UIResourceRenderer.js +250 -202
- package/dist/components/UIResourceRenderer.js.map +1 -1
- package/dist/components/chart-data-table.cjs +30 -5
- package/dist/components/chart-data-table.cjs.map +1 -1
- package/dist/components/chart-data-table.d.ts +18 -1
- package/dist/components/chart-data-table.d.ts.map +1 -1
- package/dist/components/chart-data-table.js +30 -5
- package/dist/components/chart-data-table.js.map +1 -1
- package/dist/components/chart-missing-plugin.cjs +194 -0
- package/dist/components/chart-missing-plugin.cjs.map +1 -0
- package/dist/components/chart-missing-plugin.d.ts +127 -0
- package/dist/components/chart-missing-plugin.d.ts.map +1 -0
- package/dist/components/chart-missing-plugin.js +194 -0
- package/dist/components/chart-missing-plugin.js.map +1 -0
- package/dist/context/MCPUIConfigContext.cjs.map +1 -1
- package/dist/context/MCPUIConfigContext.d.ts +57 -28
- package/dist/context/MCPUIConfigContext.d.ts.map +1 -1
- package/dist/context/MCPUIConfigContext.js.map +1 -1
- package/dist/context/MCPUIMathContext.cjs +19 -0
- package/dist/context/MCPUIMathContext.cjs.map +1 -0
- package/dist/context/MCPUIMathContext.d.ts +18 -0
- package/dist/context/MCPUIMathContext.d.ts.map +1 -0
- package/dist/context/MCPUIMathContext.js +19 -0
- package/dist/context/MCPUIMathContext.js.map +1 -0
- package/dist/context/MCPUIStringsContext.cjs +12 -1
- package/dist/context/MCPUIStringsContext.cjs.map +1 -1
- package/dist/context/MCPUIStringsContext.d.ts +62 -0
- package/dist/context/MCPUIStringsContext.d.ts.map +1 -1
- package/dist/context/MCPUIStringsContext.js +12 -1
- package/dist/context/MCPUIStringsContext.js.map +1 -1
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/plugins/katex.cjs +23 -0
- package/dist/plugins/katex.cjs.map +1 -0
- package/dist/plugins/katex.d.cts +7 -0
- package/dist/plugins/katex.d.ts +7 -0
- package/dist/plugins/katex.d.ts.map +1 -0
- package/dist/plugins/katex.js +23 -0
- package/dist/plugins/katex.js.map +1 -0
- package/dist/services/component-registry.cjs +9 -2
- package/dist/services/component-registry.cjs.map +1 -1
- package/dist/services/component-registry.d.ts.map +1 -1
- package/dist/services/component-registry.js +9 -2
- package/dist/services/component-registry.js.map +1 -1
- package/dist/services/validation.cjs +31 -8
- package/dist/services/validation.cjs.map +1 -1
- package/dist/services/validation.d.ts.map +1 -1
- package/dist/services/validation.js +31 -8
- package/dist/services/validation.js.map +1 -1
- package/dist/types/index.d.ts +14 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/degraded-projections.cjs +19 -3
- package/dist/utils/degraded-projections.cjs.map +1 -1
- package/dist/utils/degraded-projections.d.ts +22 -2
- package/dist/utils/degraded-projections.d.ts.map +1 -1
- package/dist/utils/degraded-projections.js +19 -3
- package/dist/utils/degraded-projections.js.map +1 -1
- package/dist/utils/iframe-coep.cjs.map +1 -1
- package/dist/utils/iframe-coep.d.ts +7 -6
- package/dist/utils/iframe-coep.d.ts.map +1 -1
- package/dist/utils/iframe-coep.js.map +1 -1
- package/dist/utils/math-markdown.cjs +240 -0
- package/dist/utils/math-markdown.cjs.map +1 -0
- package/dist/utils/math-markdown.d.ts +16 -0
- package/dist/utils/math-markdown.d.ts.map +1 -0
- package/dist/utils/math-markdown.js +240 -0
- package/dist/utils/math-markdown.js.map +1 -0
- package/dist/utils/missing-value.cjs +60 -0
- package/dist/utils/missing-value.cjs.map +1 -0
- package/dist/utils/missing-value.d.ts +79 -0
- package/dist/utils/missing-value.d.ts.map +1 -0
- package/dist/utils/missing-value.js +60 -0
- package/dist/utils/missing-value.js.map +1 -0
- package/package.json +23 -4
- package/src/components/ChartJSRenderer.tsx +159 -6
- package/src/components/DegradedFallback.tsx +24 -2
- package/src/components/MissingValue.tsx +61 -0
- package/src/components/UIResourceRenderer.tsx +64 -10
- package/src/components/chart-data-table.ts +50 -6
- package/src/components/chart-missing-plugin.ts +334 -0
- package/src/context/MCPUIConfigContext.tsx +57 -28
- package/src/context/MCPUIMathContext.tsx +32 -0
- package/src/context/MCPUIStringsContext.tsx +76 -0
- package/src/index.ts +4 -0
- package/src/plugins/katex.ts +25 -0
- package/src/services/component-registry.ts +10 -2
- package/src/services/validation.ts +44 -9
- package/src/types/index.ts +13 -1
- package/src/utils/degraded-projections.ts +44 -4
- package/src/utils/iframe-coep.ts +7 -6
- package/src/utils/math-markdown.ts +222 -0
- 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
|
-
//
|
|
521
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
|
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
|
}
|