@marimo-team/frontend 0.23.2-dev65 → 0.23.2-dev68

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 (31) hide show
  1. package/dist/assets/JsonOutput-sWUD3O5F.js +49 -0
  2. package/dist/assets/{add-connection-dialog-eBEz1Rlx.js → add-connection-dialog-D6XFTnTb.js} +1 -1
  3. package/dist/assets/{agent-panel-DdQIPV4s.js → agent-panel-D7n2noTX.js} +1 -1
  4. package/dist/assets/{cell-editor-DTCP63YJ.js → cell-editor-BUrhiKcX.js} +1 -1
  5. package/dist/assets/{column-preview-M5tW9DeW.js → column-preview-BbSZl4gC.js} +1 -1
  6. package/dist/assets/{command-palette-ExiY3B81.js → command-palette-Cc7XXZHG.js} +1 -1
  7. package/dist/assets/{edit-page-BTwAqki-.js → edit-page-CK943x81.js} +3 -3
  8. package/dist/assets/{file-explorer-panel-CuPnqR_c.js → file-explorer-panel-B5sFxYu1.js} +1 -1
  9. package/dist/assets/{form-D76nZZF7.js → form-BUVFJb5I.js} +1 -1
  10. package/dist/assets/{hooks-BSyCiOiV.js → hooks-CK1ac3iV.js} +1 -1
  11. package/dist/assets/index-BuOIqA8d.css +2 -0
  12. package/dist/assets/{index-BRf5dlJi.js → index-DCq7udug.js} +3 -3
  13. package/dist/assets/{layout-BULJlgp-.js → layout-0Xp7ABJw.js} +3 -3
  14. package/dist/assets/{panels-D7Ix_ZZ-.js → panels-s8gQr6G_.js} +1 -1
  15. package/dist/assets/{reveal-component-BFqPeIsQ.js → reveal-component-CT2scTNG.js} +1 -1
  16. package/dist/assets/{run-page-LlaD7bx4.js → run-page-BebSJHhp.js} +1 -1
  17. package/dist/assets/{scratchpad-panel-DEAn87uP.js → scratchpad-panel-CPn_yM49.js} +1 -1
  18. package/dist/assets/{session-panel-DrVK3__u.js → session-panel-CfLYfSX2.js} +1 -1
  19. package/dist/assets/{slide-szdKdcoM.js → slide-CdqatvaH.js} +1 -1
  20. package/dist/assets/{state-Do4CKYxK.js → state-BFFSWzNE.js} +1 -1
  21. package/dist/assets/{useNotebookActions-maZjRwJd.js → useNotebookActions-BPt4w3KQ.js} +1 -1
  22. package/dist/index.html +6 -6
  23. package/package.json +1 -1
  24. package/src/components/data-table/__tests__/columns.test.tsx +104 -0
  25. package/src/components/data-table/__tests__/sentinel-cell.test.tsx +89 -1
  26. package/src/components/data-table/__tests__/utils.test.ts +99 -0
  27. package/src/components/data-table/columns.tsx +34 -5
  28. package/src/components/data-table/sentinel-cell.tsx +34 -6
  29. package/src/components/data-table/utils.ts +45 -0
  30. package/dist/assets/JsonOutput-B7SdSwth.js +0 -49
  31. package/dist/assets/index-CD0jEnvj.css +0 -2
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { render } from "@testing-library/react";
4
4
  import { describe, expect, it } from "vitest";
5
- import { SentinelCell } from "../sentinel-cell";
5
+ import { SentinelCell, WhitespaceMarkers } from "../sentinel-cell";
6
6
  import type { CellValueSentinel } from "../types";
7
7
 
8
8
  function renderSentinel(sentinel: CellValueSentinel) {
@@ -10,6 +10,10 @@ function renderSentinel(sentinel: CellValueSentinel) {
10
10
  return container.querySelector("span")!;
11
11
  }
12
12
 
13
+ function renderMarkers(value: string) {
14
+ return render(<WhitespaceMarkers value={value} />);
15
+ }
16
+
13
17
  describe("SentinelCell", () => {
14
18
  it("renders null as None", () => {
15
19
  const span = renderSentinel({ type: "null", value: null });
@@ -81,3 +85,87 @@ describe("SentinelCell", () => {
81
85
  expect(span.getAttribute("title")).toBe("NaT (Not a Time)");
82
86
  });
83
87
  });
88
+
89
+ describe("WhitespaceMarkers", () => {
90
+ it("renders nothing for empty string", () => {
91
+ const { container } = renderMarkers("");
92
+ expect(container.firstChild).toBeNull();
93
+ });
94
+
95
+ it("renders a single space as open box", () => {
96
+ const { container } = renderMarkers(" ");
97
+ const outer = container.querySelector("span")!;
98
+ expect(outer.textContent).toBe("\u2423");
99
+ expect(outer.getAttribute("aria-label")).toBe("1 space");
100
+ });
101
+
102
+ it("renders multiple spaces as multiple open boxes", () => {
103
+ const { container } = renderMarkers(" ");
104
+ const outer = container.querySelector("span")!;
105
+ expect(outer.textContent).toBe("\u2423\u2423\u2423");
106
+ expect(outer.getAttribute("aria-label")).toBe("3 spaces");
107
+ });
108
+
109
+ it("renders tab, newline, CR with escape labels", () => {
110
+ const { container } = renderMarkers("\t\n\r");
111
+ const outer = container.querySelector("span")!;
112
+ expect(outer.textContent).toBe("\\t\\n\\r");
113
+ });
114
+
115
+ it("renders each char in its own span for CSS spacing", () => {
116
+ const { container } = renderMarkers(" ");
117
+ const outer = container.querySelector("span")!;
118
+ // Outer wrapper + three inner spans (one per char)
119
+ expect(outer.querySelectorAll("span")).toHaveLength(3);
120
+ });
121
+
122
+ it("renders unknown whitespace (NBSP) as \\uXXXX escape", () => {
123
+ const { container } = renderMarkers("\u00a0");
124
+ const outer = container.querySelector("span")!;
125
+ expect(outer.textContent).toBe("\\u00a0");
126
+ });
127
+
128
+ it("renders BOM as \\ufeff", () => {
129
+ const { container } = renderMarkers("\ufeff");
130
+ const outer = container.querySelector("span")!;
131
+ expect(outer.textContent).toBe("\\ufeff");
132
+ });
133
+
134
+ it("renders en space and em space as escapes", () => {
135
+ const { container } = renderMarkers("\u2002\u2003");
136
+ const outer = container.querySelector("span")!;
137
+ expect(outer.textContent).toBe("\\u2002\\u2003");
138
+ });
139
+
140
+ it("mixes known glyphs and unknown escapes correctly", () => {
141
+ const { container } = renderMarkers(" \t\u00a0");
142
+ const outer = container.querySelector("span")!;
143
+ expect(outer.textContent).toBe("\u2423\\t\\u00a0");
144
+ });
145
+
146
+ it("describes mixed whitespace in aria-label", () => {
147
+ const { container } = renderMarkers(" \t\n");
148
+ const outer = container.querySelector("span")!;
149
+ expect(outer.getAttribute("aria-label")).toBe("1 space, 1 tab, 1 newline");
150
+ });
151
+
152
+ it("describes unknown whitespace as 'unicode whitespace'", () => {
153
+ const { container } = renderMarkers("\u00a0");
154
+ const outer = container.querySelector("span")!;
155
+ expect(outer.getAttribute("aria-label")).toBe("1 unicode whitespace");
156
+ });
157
+
158
+ it("pluralizes unknown whitespace in aria-label", () => {
159
+ const { container } = renderMarkers("\u00a0\u00a0\u2002");
160
+ const outer = container.querySelector("span")!;
161
+ expect(outer.getAttribute("aria-label")).toBe("3 unicode whitespaces");
162
+ });
163
+
164
+ it("mixes known and unknown whitespace labels", () => {
165
+ const { container } = renderMarkers(" \u00a0\t");
166
+ const outer = container.querySelector("span")!;
167
+ expect(outer.getAttribute("aria-label")).toBe(
168
+ "1 space, 1 unicode whitespace, 1 tab",
169
+ );
170
+ });
171
+ });
@@ -7,6 +7,7 @@ import {
7
7
  getClipboardContent,
8
8
  getPageIndexForRow,
9
9
  getRawValue,
10
+ splitLeadingTrailingWhitespace,
10
11
  stringifyUnknownValue,
11
12
  } from "../utils";
12
13
 
@@ -342,3 +343,101 @@ describe("getRawValue", () => {
342
343
  expect(getRawValue(table, 5, "a")).toBeUndefined();
343
344
  });
344
345
  });
346
+
347
+ describe("splitLeadingTrailingWhitespace", () => {
348
+ it("returns all empty for empty string", () => {
349
+ expect(splitLeadingTrailingWhitespace("")).toEqual({
350
+ leading: "",
351
+ middle: "",
352
+ trailing: "",
353
+ });
354
+ });
355
+
356
+ it("returns value as middle when no edge whitespace", () => {
357
+ expect(splitLeadingTrailingWhitespace("abc")).toEqual({
358
+ leading: "",
359
+ middle: "abc",
360
+ trailing: "",
361
+ });
362
+ });
363
+
364
+ it("preserves inner whitespace in middle", () => {
365
+ expect(splitLeadingTrailingWhitespace("abc d ef")).toEqual({
366
+ leading: "",
367
+ middle: "abc d ef",
368
+ trailing: "",
369
+ });
370
+ });
371
+
372
+ it("splits leading whitespace only", () => {
373
+ expect(splitLeadingTrailingWhitespace(" abc")).toEqual({
374
+ leading: " ",
375
+ middle: "abc",
376
+ trailing: "",
377
+ });
378
+ });
379
+
380
+ it("splits trailing whitespace only", () => {
381
+ expect(splitLeadingTrailingWhitespace("abc ")).toEqual({
382
+ leading: "",
383
+ middle: "abc",
384
+ trailing: " ",
385
+ });
386
+ });
387
+
388
+ it("splits both leading and trailing whitespace", () => {
389
+ expect(splitLeadingTrailingWhitespace(" abc ")).toEqual({
390
+ leading: " ",
391
+ middle: "abc",
392
+ trailing: " ",
393
+ });
394
+ });
395
+
396
+ it("handles mixed whitespace types at edges", () => {
397
+ expect(splitLeadingTrailingWhitespace("\t\n abc \r\t")).toEqual({
398
+ leading: "\t\n ",
399
+ middle: "abc",
400
+ trailing: " \r\t",
401
+ });
402
+ });
403
+
404
+ it("preserves inner whitespace when edges have whitespace", () => {
405
+ expect(splitLeadingTrailingWhitespace(" a b c ")).toEqual({
406
+ leading: " ",
407
+ middle: "a b c",
408
+ trailing: " ",
409
+ });
410
+ });
411
+
412
+ it("handles Unicode whitespace (NBSP) at edges", () => {
413
+ expect(splitLeadingTrailingWhitespace("\u00a0abc\u00a0")).toEqual({
414
+ leading: "\u00a0",
415
+ middle: "abc",
416
+ trailing: "\u00a0",
417
+ });
418
+ });
419
+
420
+ it("puts whitespace-only string in leading (caller should handle sentinel first)", () => {
421
+ expect(splitLeadingTrailingWhitespace(" ")).toEqual({
422
+ leading: " ",
423
+ middle: "",
424
+ trailing: "",
425
+ });
426
+ });
427
+
428
+ it("handles single whitespace char", () => {
429
+ expect(splitLeadingTrailingWhitespace(" ")).toEqual({
430
+ leading: " ",
431
+ middle: "",
432
+ trailing: "",
433
+ });
434
+ });
435
+
436
+ it("handles single non-whitespace char", () => {
437
+ expect(splitLeadingTrailingWhitespace("a")).toEqual({
438
+ leading: "",
439
+ middle: "a",
440
+ trailing: "",
441
+ });
442
+ });
443
+ });
@@ -39,8 +39,8 @@ import {
39
39
  INDEX_COLUMN_NAME,
40
40
  isNumericType,
41
41
  } from "./types";
42
- import { SentinelCell } from "./sentinel-cell";
43
- import { detectSentinel } from "./utils";
42
+ import { SentinelCell, WhitespaceMarkers } from "./sentinel-cell";
43
+ import { detectSentinel, splitLeadingTrailingWhitespace } from "./utils";
44
44
  import { uniformSample } from "./uniformSample";
45
45
  import { MarkdownUrlDetector, UrlDetector } from "./url-detector";
46
46
 
@@ -346,6 +346,7 @@ const PopoutColumn = ({
346
346
  cellStyles,
347
347
  selectCell,
348
348
  rawStringValue,
349
+ edges,
349
350
  contentClassName,
350
351
  buttonText,
351
352
  wrapped,
@@ -354,11 +355,25 @@ const PopoutColumn = ({
354
355
  cellStyles?: string;
355
356
  selectCell?: () => void;
356
357
  rawStringValue: string;
358
+ // Edge whitespace shown as visible markers in the trigger; copy/title
359
+ // still use `rawStringValue`. Middle is sliced from `rawStringValue`.
360
+ edges?: { leading: string; trailing: string };
357
361
  contentClassName?: string;
358
362
  buttonText?: string;
359
363
  wrapped?: boolean;
360
364
  children: React.ReactNode;
361
365
  }) => {
366
+ const hasEdgeWhitespace =
367
+ edges !== undefined &&
368
+ (edges.leading.length > 0 || edges.trailing.length > 0);
369
+
370
+ const displayText = hasEdgeWhitespace
371
+ ? rawStringValue.slice(
372
+ edges.leading.length,
373
+ rawStringValue.length - edges.trailing.length,
374
+ )
375
+ : rawStringValue;
376
+
362
377
  return (
363
378
  <EmotionCacheProvider container={null}>
364
379
  <Popover>
@@ -377,7 +392,9 @@ const PopoutColumn = ({
377
392
  )}
378
393
  title={rawStringValue}
379
394
  >
380
- {rawStringValue}
395
+ {edges ? <WhitespaceMarkers value={edges.leading} /> : null}
396
+ {displayText}
397
+ {edges ? <WhitespaceMarkers value={edges.trailing} /> : null}
381
398
  </span>
382
399
  </PopoverTrigger>
383
400
  <PopoverContent
@@ -582,7 +599,13 @@ export function renderCellValue<TData, TValue>({
582
599
  ? String(column.applyColumnFormatting(value))
583
600
  : String(renderValue());
584
601
 
585
- const parts = parseContent(stringValue);
602
+ const { leading, middle, trailing } =
603
+ splitLeadingTrailingWhitespace(stringValue);
604
+ const hasEdgeWhitespace = leading.length > 0 || trailing.length > 0;
605
+
606
+ // Parse only the inner content for URL detection so URLDetector doesn't
607
+ // split on the whitespace padding.
608
+ const parts = parseContent(hasEdgeWhitespace ? middle : stringValue);
586
609
  const allMarkup = parts.every((part) => part.type !== "text");
587
610
  if (allMarkup || stringValue.length < MAX_STRING_LENGTH || isWrapped) {
588
611
  return (
@@ -590,7 +613,9 @@ export function renderCellValue<TData, TValue>({
590
613
  onClick={selectCell}
591
614
  className={cn(cellStyles, isWrapped && COLUMN_WRAPPING_STYLES)}
592
615
  >
616
+ <WhitespaceMarkers value={leading} />
593
617
  <UrlDetector parts={parts} />
618
+ <WhitespaceMarkers value={trailing} />
594
619
  </div>
595
620
  );
596
621
  }
@@ -600,11 +625,15 @@ export function renderCellValue<TData, TValue>({
600
625
  cellStyles={cellStyles}
601
626
  selectCell={selectCell}
602
627
  rawStringValue={stringValue}
628
+ edges={{ leading, trailing }}
603
629
  contentClassName="max-h-64 overflow-auto whitespace-pre-wrap break-words text-sm w-96"
604
630
  buttonText="X"
605
631
  wrapped={isWrapped}
606
632
  >
607
- <MarkdownUrlDetector content={stringValue} parts={parts} />
633
+ <MarkdownUrlDetector
634
+ content={stringValue}
635
+ parts={parseContent(stringValue)}
636
+ />
608
637
  </PopoutColumn>
609
638
  );
610
639
  }
@@ -3,20 +3,30 @@
3
3
  import type { CellValueSentinel, CellValueSentinelType } from "./types";
4
4
 
5
5
  const WHITESPACE_CHARS: Record<string, { marker: string; name: string }> = {
6
- " ": { marker: "\u2423", name: "space" }, // open box (space symbol)
6
+ " ": { marker: "\u2423", name: "space" },
7
7
  "\t": { marker: "\\t", name: "tab" },
8
8
  "\n": { marker: "\\n", name: "newline" },
9
- "\r": { marker: "\\r", name: "newline" },
9
+ "\r": { marker: "\\r", name: "carriage return" },
10
10
  };
11
11
 
12
- function renderWhitespaceMarkers(str: string): string {
13
- return [...str].map((ch) => WHITESPACE_CHARS[ch]?.marker ?? ch).join("");
12
+ function renderWhitespaceMarkers(str: string): React.ReactNode[] {
13
+ return [...str].map((ch, i) => {
14
+ const entry = WHITESPACE_CHARS[ch];
15
+ const marker = entry
16
+ ? entry.marker
17
+ : `\\u${(ch.codePointAt(0) ?? 0).toString(16).padStart(4, "0")}`;
18
+ return (
19
+ <span key={i} className="mr-0.5 last:mr-0">
20
+ {marker}
21
+ </span>
22
+ );
23
+ });
14
24
  }
15
25
 
16
26
  function describeWhitespace(str: string): string {
17
27
  const counts: Record<string, number> = {};
18
28
  for (const ch of str) {
19
- const name = WHITESPACE_CHARS[ch]?.name ?? "character";
29
+ const name = WHITESPACE_CHARS[ch]?.name ?? "unicode whitespace";
20
30
  counts[name] = (counts[name] ?? 0) + 1;
21
31
  }
22
32
  return Object.entries(counts)
@@ -25,7 +35,7 @@ function describeWhitespace(str: string): string {
25
35
  }
26
36
 
27
37
  interface SentinelConfig {
28
- label: (value: CellValueSentinel["value"]) => string;
38
+ label: (value: CellValueSentinel["value"]) => string | React.ReactNode[];
29
39
  tooltip: (value: CellValueSentinel["value"]) => string;
30
40
  ariaLabel: (value: CellValueSentinel["value"]) => string;
31
41
  }
@@ -68,6 +78,24 @@ const SENTINEL_CONFIG: Record<CellValueSentinelType, SentinelConfig> = {
68
78
  },
69
79
  };
70
80
 
81
+ export function WhitespaceMarkers({ value }: { value: string }) {
82
+ if (!value) {
83
+ return null;
84
+ }
85
+
86
+ const description = describeWhitespace(value);
87
+
88
+ return (
89
+ <span
90
+ className="text-muted-foreground opacity-60"
91
+ aria-label={description}
92
+ title={description}
93
+ >
94
+ {renderWhitespaceMarkers(value)}
95
+ </span>
96
+ );
97
+ }
98
+
71
99
  export function SentinelCell({
72
100
  sentinel,
73
101
  }: {
@@ -14,6 +14,51 @@ import {
14
14
  } from "./types";
15
15
 
16
16
  const WHITESPACE_ONLY_RE = /^[\s]+$/;
17
+ const WHITESPACE_CHAR_RE = /\s/;
18
+ const EDGE_WHITESPACE_RE = /^(\s*)([\s\S]*?)(\s*)$/;
19
+
20
+ /**
21
+ * checks for leading and trailing whitespaces.
22
+ * Will run for every cell, so fast exits for common cases
23
+ *
24
+ * @param value - to split
25
+ * @returns - leading, middle, and trailing string where leading and trailing are whitespace string
26
+ */
27
+ export function splitLeadingTrailingWhitespace(value: string): {
28
+ leading: string;
29
+ middle: string;
30
+ trailing: string;
31
+ } {
32
+ const parts = {
33
+ leading: "",
34
+ middle: "",
35
+ trailing: "",
36
+ };
37
+ if (value.length === 0) {
38
+ return parts;
39
+ }
40
+
41
+ const firstWhitespaceCh = WHITESPACE_CHAR_RE.test(value[0]);
42
+ const lastWhitespaceCh = WHITESPACE_CHAR_RE.test(value[value.length - 1]);
43
+
44
+ // if does not start or end with ws
45
+ if (!firstWhitespaceCh && !lastWhitespaceCh) {
46
+ parts.middle = value;
47
+ return parts;
48
+ }
49
+
50
+ const match = EDGE_WHITESPACE_RE.exec(value);
51
+ if (!match) {
52
+ parts.middle = value;
53
+ return parts;
54
+ }
55
+
56
+ parts.leading = match[1] ?? "";
57
+ parts.middle = match[2] ?? "";
58
+ parts.trailing = match[3] ?? "";
59
+
60
+ return parts;
61
+ }
17
62
 
18
63
  /**
19
64
  * Convenience function to load table data.