@marimo-team/islands 0.23.2-dev33 → 0.23.2-dev37
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/dist/{chat-ui-CVKwVnwD.js → chat-ui-CqkLwgnX.js} +1 -1
- package/dist/main.js +233 -80
- package/dist/{process-output-BX1UfKIX.js → process-output-Db_3pJA4.js} +2115 -2111
- package/package.json +1 -1
- package/src/components/data-table/__tests__/sentinel-cell.test.tsx +83 -0
- package/src/components/data-table/__tests__/utils.test.ts +128 -0
- package/src/components/data-table/column-header.tsx +11 -2
- package/src/components/data-table/columns.tsx +16 -2
- package/src/components/data-table/sentinel-cell.tsx +90 -0
- package/src/components/data-table/types.ts +23 -0
- package/src/components/data-table/utils.ts +74 -1
- package/src/components/ui/toast.tsx +16 -7
package/package.json
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { render } from "@testing-library/react";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { SentinelCell } from "../sentinel-cell";
|
|
6
|
+
import type { CellValueSentinel } from "../types";
|
|
7
|
+
|
|
8
|
+
function renderSentinel(sentinel: CellValueSentinel) {
|
|
9
|
+
const { container } = render(<SentinelCell sentinel={sentinel} />);
|
|
10
|
+
return container.querySelector("span")!;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("SentinelCell", () => {
|
|
14
|
+
it("renders null as None", () => {
|
|
15
|
+
const span = renderSentinel({ type: "null", value: null });
|
|
16
|
+
expect(span.textContent).toBe("None");
|
|
17
|
+
expect(span.getAttribute("aria-label")).toBe("None");
|
|
18
|
+
expect(span.className).toContain("italic");
|
|
19
|
+
expect(span.className).toContain("bg-muted");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("renders empty string as <empty>", () => {
|
|
23
|
+
const span = renderSentinel({ type: "empty-string", value: "" });
|
|
24
|
+
expect(span.textContent).toBe("<empty>");
|
|
25
|
+
expect(span.getAttribute("aria-label")).toBe("empty string");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("renders single space", () => {
|
|
29
|
+
const span = renderSentinel({ type: "whitespace", value: " " });
|
|
30
|
+
expect(span.textContent).toBe("\u2423");
|
|
31
|
+
expect(span.getAttribute("aria-label")).toBe("1 space");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("renders multiple spaces", () => {
|
|
35
|
+
const span = renderSentinel({ type: "whitespace", value: " " });
|
|
36
|
+
expect(span.textContent).toBe("\u2423\u2423\u2423");
|
|
37
|
+
expect(span.getAttribute("aria-label")).toBe("3 spaces");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("renders tab", () => {
|
|
41
|
+
const span = renderSentinel({ type: "whitespace", value: "\t" });
|
|
42
|
+
expect(span.textContent).toBe("\\t");
|
|
43
|
+
expect(span.getAttribute("aria-label")).toBe("1 tab");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("renders newline", () => {
|
|
47
|
+
const span = renderSentinel({ type: "whitespace", value: "\n" });
|
|
48
|
+
expect(span.textContent).toBe("\\n");
|
|
49
|
+
expect(span.getAttribute("aria-label")).toBe("1 newline");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("renders mixed whitespace", () => {
|
|
53
|
+
const span = renderSentinel({ type: "whitespace", value: "\t \n" });
|
|
54
|
+
expect(span.textContent).toBe("\\t\u2423\\n");
|
|
55
|
+
expect(span.getAttribute("aria-label")).toBe("1 tab, 1 space, 1 newline");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("renders NaN", () => {
|
|
59
|
+
const span = renderSentinel({ type: "nan", value: Number.NaN });
|
|
60
|
+
expect(span.textContent).toBe("NaN");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("renders inf", () => {
|
|
64
|
+
const span = renderSentinel({ type: "positive-infinity", value: Infinity });
|
|
65
|
+
expect(span.textContent).toBe("inf");
|
|
66
|
+
expect(span.getAttribute("title")).toBe("Infinity");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("renders -inf", () => {
|
|
70
|
+
const span = renderSentinel({
|
|
71
|
+
type: "negative-infinity",
|
|
72
|
+
value: -Infinity,
|
|
73
|
+
});
|
|
74
|
+
expect(span.textContent).toBe("-inf");
|
|
75
|
+
expect(span.getAttribute("title")).toBe("-Infinity");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("renders NaT", () => {
|
|
79
|
+
const span = renderSentinel({ type: "nat", value: "NaT" });
|
|
80
|
+
expect(span.textContent).toBe("NaT");
|
|
81
|
+
expect(span.getAttribute("title")).toBe("NaT (Not a Time)");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { Table } from "@tanstack/react-table";
|
|
4
4
|
import { describe, expect, it } from "vitest";
|
|
5
5
|
import {
|
|
6
|
+
detectSentinel,
|
|
6
7
|
getClipboardContent,
|
|
7
8
|
getPageIndexForRow,
|
|
8
9
|
getRawValue,
|
|
@@ -186,6 +187,133 @@ describe("getClipboardContent", () => {
|
|
|
186
187
|
});
|
|
187
188
|
});
|
|
188
189
|
|
|
190
|
+
describe("detectSentinel", () => {
|
|
191
|
+
it("should detect null and undefined", () => {
|
|
192
|
+
expect(detectSentinel(null, undefined)).toEqual({
|
|
193
|
+
type: "null",
|
|
194
|
+
value: null,
|
|
195
|
+
});
|
|
196
|
+
expect(detectSentinel(undefined, undefined)).toEqual({
|
|
197
|
+
type: "null",
|
|
198
|
+
value: undefined,
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("should detect empty string", () => {
|
|
203
|
+
expect(detectSentinel("", "string")).toEqual({
|
|
204
|
+
type: "empty-string",
|
|
205
|
+
value: "",
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("should detect whitespace-only strings", () => {
|
|
210
|
+
expect(detectSentinel(" ", "string")).toEqual({
|
|
211
|
+
type: "whitespace",
|
|
212
|
+
value: " ",
|
|
213
|
+
});
|
|
214
|
+
expect(detectSentinel(" ", "string")).toEqual({
|
|
215
|
+
type: "whitespace",
|
|
216
|
+
value: " ",
|
|
217
|
+
});
|
|
218
|
+
expect(detectSentinel("\t", "string")).toEqual({
|
|
219
|
+
type: "whitespace",
|
|
220
|
+
value: "\t",
|
|
221
|
+
});
|
|
222
|
+
expect(detectSentinel("\n", "string")).toEqual({
|
|
223
|
+
type: "whitespace",
|
|
224
|
+
value: "\n",
|
|
225
|
+
});
|
|
226
|
+
expect(detectSentinel("\t \n", "string")).toEqual({
|
|
227
|
+
type: "whitespace",
|
|
228
|
+
value: "\t \n",
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("should detect NaN", () => {
|
|
233
|
+
expect(detectSentinel(Number.NaN, "number")).toEqual({
|
|
234
|
+
type: "nan",
|
|
235
|
+
value: Number.NaN,
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("should detect Infinity", () => {
|
|
240
|
+
expect(detectSentinel(Number.POSITIVE_INFINITY, "number")).toEqual({
|
|
241
|
+
type: "positive-infinity",
|
|
242
|
+
value: Number.POSITIVE_INFINITY,
|
|
243
|
+
});
|
|
244
|
+
expect(detectSentinel(Number.NEGATIVE_INFINITY, "number")).toEqual({
|
|
245
|
+
type: "negative-infinity",
|
|
246
|
+
value: Number.NEGATIVE_INFINITY,
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("should return null for normal values", () => {
|
|
251
|
+
expect(detectSentinel("hello", "string")).toBeNull();
|
|
252
|
+
expect(detectSentinel(42, "number")).toBeNull();
|
|
253
|
+
expect(detectSentinel(0, "number")).toBeNull();
|
|
254
|
+
expect(detectSentinel(-1.5, "number")).toBeNull();
|
|
255
|
+
expect(detectSentinel(true, "boolean")).toBeNull();
|
|
256
|
+
expect(detectSentinel(false, "boolean")).toBeNull();
|
|
257
|
+
expect(detectSentinel({}, "unknown")).toBeNull();
|
|
258
|
+
expect(detectSentinel([], "unknown")).toBeNull();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("should not match literal null-like strings", () => {
|
|
262
|
+
expect(detectSentinel("null", "string")).toBeNull();
|
|
263
|
+
expect(detectSentinel("NULL", "string")).toBeNull();
|
|
264
|
+
expect(detectSentinel("None", "string")).toBeNull();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("should not match string NaN/Infinity in non-numeric columns", () => {
|
|
268
|
+
expect(detectSentinel("NaN", "string")).toBeNull();
|
|
269
|
+
expect(detectSentinel("Infinity", "string")).toBeNull();
|
|
270
|
+
expect(detectSentinel("-Infinity", "string")).toBeNull();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("should match string NaN/Infinity in numeric columns", () => {
|
|
274
|
+
expect(detectSentinel("NaN", "number")).toEqual({
|
|
275
|
+
type: "nan",
|
|
276
|
+
value: "NaN",
|
|
277
|
+
});
|
|
278
|
+
expect(detectSentinel("Infinity", "number")).toEqual({
|
|
279
|
+
type: "positive-infinity",
|
|
280
|
+
value: "Infinity",
|
|
281
|
+
});
|
|
282
|
+
expect(detectSentinel("-Infinity", "number")).toEqual({
|
|
283
|
+
type: "negative-infinity",
|
|
284
|
+
value: "-Infinity",
|
|
285
|
+
});
|
|
286
|
+
expect(detectSentinel("inf", "number")).toEqual({
|
|
287
|
+
type: "positive-infinity",
|
|
288
|
+
value: "inf",
|
|
289
|
+
});
|
|
290
|
+
expect(detectSentinel("-inf", "number")).toEqual({
|
|
291
|
+
type: "negative-infinity",
|
|
292
|
+
value: "-inf",
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("should still not match normal strings in numeric columns", () => {
|
|
297
|
+
expect(detectSentinel("hello", "number")).toBeNull();
|
|
298
|
+
expect(detectSentinel("42", "number")).toBeNull();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("should not match NaT in non-temporal columns", () => {
|
|
302
|
+
expect(detectSentinel("NaT", "string")).toBeNull();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("should match NaT in temporal columns", () => {
|
|
306
|
+
expect(detectSentinel("NaT", "datetime")).toEqual({
|
|
307
|
+
type: "nat",
|
|
308
|
+
value: "NaT",
|
|
309
|
+
});
|
|
310
|
+
expect(detectSentinel("NaT", "date")).toEqual({
|
|
311
|
+
type: "nat",
|
|
312
|
+
value: "NaT",
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
189
317
|
function createMockTableWithMeta<TData>(rawData?: TData[]): Table<TData> {
|
|
190
318
|
return {
|
|
191
319
|
options: {
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
SelectValue,
|
|
54
54
|
} from "../ui/select";
|
|
55
55
|
import { type ColumnFilterForType, Filter } from "./filters";
|
|
56
|
+
import { SentinelCell } from "./sentinel-cell";
|
|
56
57
|
import {
|
|
57
58
|
ClearFilterMenuItem,
|
|
58
59
|
FilterButtons,
|
|
@@ -65,7 +66,7 @@ import {
|
|
|
65
66
|
renderSortFilterIcon,
|
|
66
67
|
renderSorts,
|
|
67
68
|
} from "./header-items";
|
|
68
|
-
import { stringifyUnknownValue } from "./utils";
|
|
69
|
+
import { detectSentinel, stringifyUnknownValue } from "./utils";
|
|
69
70
|
|
|
70
71
|
const TOP_K_ROWS = 30;
|
|
71
72
|
|
|
@@ -647,6 +648,10 @@ const PopoverFilterByValues = <TData, TValue>({
|
|
|
647
648
|
{filteredData.map(([value, count], rowIndex) => {
|
|
648
649
|
const isSelected = chosenValues.has(value);
|
|
649
650
|
const valueString = stringifyUnknownValue({ value });
|
|
651
|
+
const sentinel = detectSentinel(
|
|
652
|
+
value,
|
|
653
|
+
column.columnDef.meta?.dataType,
|
|
654
|
+
);
|
|
650
655
|
|
|
651
656
|
return (
|
|
652
657
|
<CommandItem
|
|
@@ -661,7 +666,11 @@ const PopoverFilterByValues = <TData, TValue>({
|
|
|
661
666
|
className="mr-3 h-3.5 w-3.5"
|
|
662
667
|
/>
|
|
663
668
|
<span className="flex-1 overflow-hidden max-h-20 line-clamp-3">
|
|
664
|
-
{
|
|
669
|
+
{sentinel ? (
|
|
670
|
+
<SentinelCell sentinel={sentinel} />
|
|
671
|
+
) : (
|
|
672
|
+
valueString
|
|
673
|
+
)}
|
|
665
674
|
</span>
|
|
666
675
|
<span className="ml-3">{count}</span>
|
|
667
676
|
</CommandItem>
|
|
@@ -37,7 +37,10 @@ import {
|
|
|
37
37
|
extractTimezone,
|
|
38
38
|
type FieldTypesWithExternalType,
|
|
39
39
|
INDEX_COLUMN_NAME,
|
|
40
|
+
isNumericType,
|
|
40
41
|
} from "./types";
|
|
42
|
+
import { SentinelCell } from "./sentinel-cell";
|
|
43
|
+
import { detectSentinel } from "./utils";
|
|
41
44
|
import { uniformSample } from "./uniformSample";
|
|
42
45
|
import { MarkdownUrlDetector, UrlDetector } from "./url-detector";
|
|
43
46
|
|
|
@@ -163,7 +166,7 @@ export function generateColumns<T>({
|
|
|
163
166
|
}
|
|
164
167
|
// Auto right-align numeric columns
|
|
165
168
|
const dataType = getMeta(key).dataType;
|
|
166
|
-
if (dataType
|
|
169
|
+
if (isNumericType(dataType)) {
|
|
167
170
|
return "right";
|
|
168
171
|
}
|
|
169
172
|
return undefined;
|
|
@@ -269,7 +272,7 @@ export function generateColumns<T>({
|
|
|
269
272
|
!isCellSelected;
|
|
270
273
|
|
|
271
274
|
const dataType = column.columnDef.meta?.dataType;
|
|
272
|
-
const isNumeric = dataType
|
|
275
|
+
const isNumeric = isNumericType(dataType);
|
|
273
276
|
const cellStyles = getCellStyleClass({
|
|
274
277
|
justify,
|
|
275
278
|
wrapped,
|
|
@@ -522,6 +525,17 @@ export function renderCellValue<TData, TValue>({
|
|
|
522
525
|
|
|
523
526
|
const isWrapped = column.getColumnWrapping?.() === "wrap";
|
|
524
527
|
|
|
528
|
+
// Sentinel values (null, whitespace, NaN, Infinity, NaT) rendered specially.
|
|
529
|
+
// Empty strings are left as-is
|
|
530
|
+
const sentinel = detectSentinel(value, dataType);
|
|
531
|
+
if (sentinel && sentinel.type !== "empty-string") {
|
|
532
|
+
return (
|
|
533
|
+
<div onClick={selectCell} className={cellStyles}>
|
|
534
|
+
<SentinelCell sentinel={sentinel} />
|
|
535
|
+
</div>
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
525
539
|
if (dataType === "datetime" && typeof value === "string") {
|
|
526
540
|
try {
|
|
527
541
|
if (!isValid(value)) {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type { CellValueSentinel, CellValueSentinelType } from "./types";
|
|
4
|
+
|
|
5
|
+
const WHITESPACE_CHARS: Record<string, { marker: string; name: string }> = {
|
|
6
|
+
" ": { marker: "\u2423", name: "space" }, // open box (space symbol)
|
|
7
|
+
"\t": { marker: "\\t", name: "tab" },
|
|
8
|
+
"\n": { marker: "\\n", name: "newline" },
|
|
9
|
+
"\r": { marker: "\\r", name: "newline" },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function renderWhitespaceMarkers(str: string): string {
|
|
13
|
+
return [...str].map((ch) => WHITESPACE_CHARS[ch]?.marker ?? ch).join("");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function describeWhitespace(str: string): string {
|
|
17
|
+
const counts: Record<string, number> = {};
|
|
18
|
+
for (const ch of str) {
|
|
19
|
+
const name = WHITESPACE_CHARS[ch]?.name ?? "character";
|
|
20
|
+
counts[name] = (counts[name] ?? 0) + 1;
|
|
21
|
+
}
|
|
22
|
+
return Object.entries(counts)
|
|
23
|
+
.map(([name, count]) => `${count} ${name}${count > 1 ? "s" : ""}`)
|
|
24
|
+
.join(", ");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface SentinelConfig {
|
|
28
|
+
label: (value: CellValueSentinel["value"]) => string;
|
|
29
|
+
tooltip: (value: CellValueSentinel["value"]) => string;
|
|
30
|
+
ariaLabel: (value: CellValueSentinel["value"]) => string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const SENTINEL_CONFIG: Record<CellValueSentinelType, SentinelConfig> = {
|
|
34
|
+
null: {
|
|
35
|
+
label: () => "None",
|
|
36
|
+
tooltip: () => "None",
|
|
37
|
+
ariaLabel: () => "None",
|
|
38
|
+
},
|
|
39
|
+
"empty-string": {
|
|
40
|
+
label: () => "<empty>",
|
|
41
|
+
tooltip: () => "<empty>",
|
|
42
|
+
ariaLabel: () => "empty string",
|
|
43
|
+
},
|
|
44
|
+
whitespace: {
|
|
45
|
+
label: (value) => renderWhitespaceMarkers(String(value)),
|
|
46
|
+
tooltip: (value) => describeWhitespace(String(value)),
|
|
47
|
+
ariaLabel: (value) => describeWhitespace(String(value)),
|
|
48
|
+
},
|
|
49
|
+
nan: {
|
|
50
|
+
label: () => "NaN",
|
|
51
|
+
tooltip: () => "NaN",
|
|
52
|
+
ariaLabel: () => "NaN",
|
|
53
|
+
},
|
|
54
|
+
"positive-infinity": {
|
|
55
|
+
label: () => "inf",
|
|
56
|
+
tooltip: () => "Infinity",
|
|
57
|
+
ariaLabel: () => "infinity",
|
|
58
|
+
},
|
|
59
|
+
"negative-infinity": {
|
|
60
|
+
label: () => "-inf",
|
|
61
|
+
tooltip: () => "-Infinity",
|
|
62
|
+
ariaLabel: () => "negative infinity",
|
|
63
|
+
},
|
|
64
|
+
nat: {
|
|
65
|
+
label: () => "NaT",
|
|
66
|
+
tooltip: () => "NaT (Not a Time)",
|
|
67
|
+
ariaLabel: () => "Not a Time",
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export function SentinelCell({
|
|
72
|
+
sentinel,
|
|
73
|
+
}: {
|
|
74
|
+
sentinel: CellValueSentinel;
|
|
75
|
+
}): React.ReactElement {
|
|
76
|
+
const config = SENTINEL_CONFIG[sentinel.type];
|
|
77
|
+
const label = config.label(sentinel.value);
|
|
78
|
+
const tooltip = config.tooltip(sentinel.value);
|
|
79
|
+
const ariaLabel = config.ariaLabel(sentinel.value);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<span
|
|
83
|
+
className="italic text-muted-foreground bg-muted rounded px-1"
|
|
84
|
+
aria-label={ariaLabel}
|
|
85
|
+
title={tooltip}
|
|
86
|
+
>
|
|
87
|
+
<span className="opacity-70">{label}</span>
|
|
88
|
+
</span>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -95,6 +95,29 @@ export type DataTableSelection =
|
|
|
95
95
|
| "multi-cell"
|
|
96
96
|
| null;
|
|
97
97
|
|
|
98
|
+
export type CellValueSentinel =
|
|
99
|
+
| { type: "null"; value: null | undefined }
|
|
100
|
+
| { type: "empty-string"; value: string }
|
|
101
|
+
| { type: "whitespace"; value: string }
|
|
102
|
+
| { type: "nan"; value: number | string }
|
|
103
|
+
| { type: "positive-infinity"; value: number | string }
|
|
104
|
+
| { type: "negative-infinity"; value: number | string }
|
|
105
|
+
| { type: "nat"; value: string };
|
|
106
|
+
|
|
107
|
+
export type CellValueSentinelType = CellValueSentinel["type"];
|
|
108
|
+
|
|
109
|
+
export function isNumericType(
|
|
110
|
+
dataType: DataType | undefined,
|
|
111
|
+
): dataType is "number" | "integer" {
|
|
112
|
+
return dataType === "number" || dataType === "integer";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function isTemporalType(
|
|
116
|
+
dataType: DataType | undefined,
|
|
117
|
+
): dataType is "date" | "datetime" | "time" {
|
|
118
|
+
return dataType === "date" || dataType === "datetime" || dataType === "time";
|
|
119
|
+
}
|
|
120
|
+
|
|
98
121
|
export function extractTimezone(dtype: string | undefined): string | undefined {
|
|
99
122
|
if (!dtype) {
|
|
100
123
|
return undefined;
|
|
@@ -5,7 +5,15 @@ import type { TableData } from "@/plugins/impl/DataTablePlugin";
|
|
|
5
5
|
import { vegaLoadData } from "@/plugins/impl/vega/loader";
|
|
6
6
|
import { jsonParseWithSpecialChar } from "@/utils/json/json-parser";
|
|
7
7
|
import { getMimeValues } from "./mime-cell";
|
|
8
|
-
import {
|
|
8
|
+
import type { DataType } from "@/core/kernel/messages";
|
|
9
|
+
import {
|
|
10
|
+
type CellValueSentinel,
|
|
11
|
+
INDEX_COLUMN_NAME,
|
|
12
|
+
isNumericType,
|
|
13
|
+
isTemporalType,
|
|
14
|
+
} from "./types";
|
|
15
|
+
|
|
16
|
+
const WHITESPACE_ONLY_RE = /^[\s]+$/;
|
|
9
17
|
|
|
10
18
|
/**
|
|
11
19
|
* Convenience function to load table data.
|
|
@@ -85,6 +93,71 @@ export function getPageIndexForRow(
|
|
|
85
93
|
return null;
|
|
86
94
|
}
|
|
87
95
|
|
|
96
|
+
// String representations of numeric special values.
|
|
97
|
+
// Only matched when the caller indicates the column is numeric.
|
|
98
|
+
type StringValueSentinelType = Extract<
|
|
99
|
+
CellValueSentinel,
|
|
100
|
+
{ value: number | string }
|
|
101
|
+
>["type"];
|
|
102
|
+
|
|
103
|
+
const NUMERIC_STRING_SPECIALS: Record<string, StringValueSentinelType> = {
|
|
104
|
+
NaN: "nan",
|
|
105
|
+
Infinity: "positive-infinity",
|
|
106
|
+
"-Infinity": "negative-infinity",
|
|
107
|
+
inf: "positive-infinity",
|
|
108
|
+
"-inf": "negative-infinity",
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Detect if a cell value is a sentinel (null, empty string, whitespace,
|
|
113
|
+
* NaN, infinity, NaT). Column-type-dependent sentinels (string "NaN",
|
|
114
|
+
* "NaT", etc.) are matched based on `dataType`.
|
|
115
|
+
*/
|
|
116
|
+
export function detectSentinel(
|
|
117
|
+
value: unknown,
|
|
118
|
+
dataType: DataType | undefined,
|
|
119
|
+
): CellValueSentinel | null {
|
|
120
|
+
if (value == null) {
|
|
121
|
+
return { type: "null", value };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (typeof value === "string") {
|
|
125
|
+
if (value === "") {
|
|
126
|
+
return { type: "empty-string", value };
|
|
127
|
+
}
|
|
128
|
+
if (WHITESPACE_ONLY_RE.test(value)) {
|
|
129
|
+
return { type: "whitespace", value };
|
|
130
|
+
}
|
|
131
|
+
// String "NaN"/"Infinity" in a numeric column = actual special float value
|
|
132
|
+
if (isNumericType(dataType)) {
|
|
133
|
+
const type = NUMERIC_STRING_SPECIALS[value];
|
|
134
|
+
if (type) {
|
|
135
|
+
return { type, value };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// String "NaT" in a temporal column = pandas Not-a-Time sentinel
|
|
139
|
+
if (isTemporalType(dataType) && value === "NaT") {
|
|
140
|
+
return { type: "nat", value };
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (typeof value === "number") {
|
|
146
|
+
if (Number.isNaN(value)) {
|
|
147
|
+
return { type: "nan", value };
|
|
148
|
+
}
|
|
149
|
+
if (value === Number.POSITIVE_INFINITY) {
|
|
150
|
+
return { type: "positive-infinity", value };
|
|
151
|
+
}
|
|
152
|
+
if (value === Number.NEGATIVE_INFINITY) {
|
|
153
|
+
return { type: "negative-infinity", value };
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
88
161
|
/**
|
|
89
162
|
* Stringify an unknown value. Converts objects to JSON strings.
|
|
90
163
|
* @param opts.value - The value to stringify.
|
|
@@ -24,32 +24,41 @@ const ToastViewport = React.forwardRef<
|
|
|
24
24
|
));
|
|
25
25
|
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
|
26
26
|
|
|
27
|
+
const VARIANT_CLASSES = {
|
|
28
|
+
default: "bg-background border",
|
|
29
|
+
danger:
|
|
30
|
+
"group destructive text-error border-destructive bg-(--red-1) shadow-md-solid shadow-error",
|
|
31
|
+
} as const satisfies Record<string, string>;
|
|
32
|
+
|
|
33
|
+
type ToastVariant = keyof typeof VARIANT_CLASSES;
|
|
34
|
+
|
|
27
35
|
const toastVariants = cva(
|
|
28
36
|
"data-[swipe=move]:transition-none group relative pointer-events-auto flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=move]:translate-x-(--radix-toast-swipe-move-x) data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-(--radix-toast-swipe-end-x) data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=open]:slide-in-from-top-full sm:data-[state=open]:slide-in-from-bottom-full data-[state=closed]:slide-out-to-right-full",
|
|
29
37
|
{
|
|
30
38
|
variants: {
|
|
31
|
-
variant:
|
|
32
|
-
default: "bg-background border",
|
|
33
|
-
danger:
|
|
34
|
-
"group destructive text-error border-destructive bg-(--red-1) shadow-md-solid shadow-error",
|
|
35
|
-
},
|
|
39
|
+
variant: VARIANT_CLASSES,
|
|
36
40
|
},
|
|
37
41
|
defaultVariants: {
|
|
38
|
-
variant: "default",
|
|
42
|
+
variant: "default" satisfies ToastVariant,
|
|
39
43
|
},
|
|
40
44
|
},
|
|
41
45
|
);
|
|
42
46
|
|
|
47
|
+
function isToastVariant(value: unknown): value is ToastVariant {
|
|
48
|
+
return typeof value === "string" && value in VARIANT_CLASSES;
|
|
49
|
+
}
|
|
50
|
+
|
|
43
51
|
const Toast = React.forwardRef<
|
|
44
52
|
React.ElementRef<typeof ToastPrimitives.Root>,
|
|
45
53
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
|
46
54
|
VariantProps<typeof toastVariants>
|
|
47
55
|
>(({ className, variant, ...props }, ref) => {
|
|
56
|
+
const resolvedVariant = isToastVariant(variant) ? variant : "default";
|
|
48
57
|
return (
|
|
49
58
|
<ToastPrimitives.Root
|
|
50
59
|
ref={ref}
|
|
51
60
|
className={cn(
|
|
52
|
-
toastVariants({ variant:
|
|
61
|
+
toastVariants({ variant: resolvedVariant }),
|
|
53
62
|
"print:hidden",
|
|
54
63
|
className,
|
|
55
64
|
)}
|