@marimo-team/islands 0.25.3-dev82 → 0.25.3-dev86

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/{ErrorBoundary-Dh-VaAao.js → ErrorBoundary-CWTKX5F9.js} +1 -1
  2. package/dist/{chat-ui-C0_lCzql.js → chat-ui-C9S_7vEJ.js} +4 -4
  3. package/dist/{code-visibility-BwTJAT1U.js → code-visibility-BYYTZhMX.js} +3 -3
  4. package/dist/{common-BTpcnvZO.js → common-D5MFwWU7.js} +5308 -5281
  5. package/dist/{data-grid-overlay-editor-C95q5QIf.js → data-grid-overlay-editor-0nyubmEm.js} +1 -1
  6. package/dist/{error-entries-CMNtA8NK.js → error-entries-Cmiq1RT-.js} +1 -1
  7. package/dist/{feedback-button-sOLkDWYm.js → feedback-button-BVXnmzuY.js} +3 -3
  8. package/dist/{formats-6pLWWrY2.js → formats-ZmooA-MD.js} +1 -1
  9. package/dist/{glide-data-editor-D6GkY0Z2.js → glide-data-editor-aaTq9XhK.js} +3 -3
  10. package/dist/{globals-rH_9s1xB.js → globals-CtT-eRE1.js} +1 -1
  11. package/dist/{html-to-image-6xcoTKhC.js → html-to-image-Dc3mTXAs.js} +2 -2
  12. package/dist/main.js +13 -13
  13. package/dist/{number-overlay-editor-BjJttHms.js → number-overlay-editor-DHSkZEYh.js} +1 -1
  14. package/dist/{process-output-BJRJFobf.js → process-output-C47Xs8_M.js} +2 -2
  15. package/dist/{reveal-component-Ckn4ABk3.js → reveal-component-CObsc45h.js} +4 -4
  16. package/dist/style.css +1 -1
  17. package/dist/{toDate-CjPuEPou.js → toDate-C6BRw8i5.js} +10 -13
  18. package/dist/{vega-component-DnfQeoyt.js → vega-component-B8IjMdad.js} +2 -2
  19. package/package.json +1 -1
  20. package/src/components/data-table/__tests__/columns.test.tsx +105 -0
  21. package/src/components/data-table/__tests__/search-highlight.test.tsx +109 -0
  22. package/src/components/data-table/__tests__/url-detector.test.tsx +74 -0
  23. package/src/components/data-table/columns.tsx +43 -45
  24. package/src/components/data-table/data-table.tsx +7 -1
  25. package/src/components/data-table/search-highlight.tsx +79 -0
  26. package/src/components/data-table/types.ts +1 -0
  27. package/src/components/data-table/url-detector.tsx +49 -9
  28. package/src/core/runtime/__tests__/createWsUrl.test.ts +77 -7
  29. package/src/core/runtime/__tests__/runtime.test.ts +13 -5
  30. package/src/core/runtime/runtime.ts +23 -27
  31. package/src/css/globals.css +2 -0
@@ -562,26 +562,23 @@ var G = new Set(Object.values(s)), K = class {
562
562
  });
563
563
  return q(this.appendCrossOriginAuth(n).toString());
564
564
  }
565
- getSessionSearchParams(e) {
566
- let t = new URL(this.config.url), n = new URLSearchParams(t.search);
567
- return new URLSearchParams(window.location.search).forEach((e, t) => {
568
- n.has(t) || n.set(t, e);
569
- }), n.set(s.sessionId, e), n;
565
+ getSessionURL(e, t) {
566
+ let n = this.formatHttpURL({ path: e }), r = new URLSearchParams(this.httpURL.search), i = new URLSearchParams(window.location.search);
567
+ for (let e of i.keys()) r.delete(e);
568
+ for (let [e, t] of i) r.append(e, t);
569
+ return r.delete(s.accessToken), r.delete("refresh_token"), r.delete(s.sessionId), n.searchParams.set("query_params", r.toString()), n.searchParams.set(s.sessionId, t), n;
570
570
  }
571
571
  getWsURL(e) {
572
- let t = this.getSessionSearchParams(e);
573
- return this.formatWsURL("/ws", t);
572
+ let t = this.getSessionURL("/ws", e);
573
+ return q(this.appendCrossOriginAuth(t).toString());
574
574
  }
575
575
  getSseURL(e) {
576
- let t = this.formatHttpURL({
577
- path: "/sse",
578
- searchParams: this.getSessionSearchParams(e),
579
- restrictToKnownQueryParams: !1
580
- });
576
+ let t = this.getSessionURL("/sse", e);
581
577
  return t.searchParams.delete(s.accessToken), t;
582
578
  }
583
579
  getWsSyncURL(e) {
584
- return this.formatWsURL("/ws_sync", this.getSessionSearchParams(e));
580
+ let t = this.getSessionURL("/ws_sync", e);
581
+ return q(this.appendCrossOriginAuth(t).toString());
585
582
  }
586
583
  getTerminalWsURL(e) {
587
584
  let t = this.formatWsURL("/terminal/ws");
@@ -3,7 +3,7 @@ import { g as t, h as n, m as r, o as i } from "./button-BxB3RDjN.js";
3
3
  import { t as a } from "./react-sHtZdjeP.js";
4
4
  import { t as o } from "./compiler-runtime-CldRdSsv.js";
5
5
  import { t as s } from "./tooltip-CKO1pHJo.js";
6
- import { C as c, a as l } from "./toDate-CjPuEPou.js";
6
+ import { C as c, a as l } from "./toDate-C6BRw8i5.js";
7
7
  import { t as u } from "./jsx-runtime-3BD1kT0s.js";
8
8
  import { n as d } from "./error-banner-Sl2FlSJ4.js";
9
9
  import { t as f } from "./debounce-C3jyATpc.js";
@@ -11,7 +11,7 @@ import { A as p, n as ee } from "./useTheme-DympCsQg.js";
11
11
  import { c as m, t as h } from "./arrays-R6JsImiT.js";
12
12
  import { t as te } from "./objects-DCmMvZtn.js";
13
13
  import { t as ne } from "./useAsyncData-ChgNtdmE.js";
14
- import { a as g, i as re, n as ie, t as ae } from "./formats-6pLWWrY2.js";
14
+ import { a as g, i as re, n as ie, t as ae } from "./formats-ZmooA-MD.js";
15
15
  import { D as oe, n as _ } from "./vega-loader.browser-B9Pz0EGP.js";
16
16
  import { t as se } from "./react-vega-C4T9Rb_w.js";
17
17
  import { a as ce, n as le } from "./loader-Bm8cy3e8.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.25.3-dev82",
3
+ "version": "0.25.3-dev86",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -14,6 +14,7 @@ import {
14
14
  renderCellValue,
15
15
  } from "../columns";
16
16
  import { getMimeValues, isMimeValue, MimeCell } from "../mime-cell";
17
+ import { buildHighlightPattern } from "../search-highlight";
17
18
  import type { FieldTypesWithExternalType } from "../types";
18
19
  import { uniformSample } from "../uniformSample";
19
20
  import { UrlDetector } from "../url-detector";
@@ -240,6 +241,7 @@ describe("generateColumns", () => {
240
241
  // "age" is a number column — should auto right-align
241
242
  // oxlint-disable-next-line typescript/no-explicit-any
242
243
  const cell = (columns[1].cell as any)({
244
+ table: { options: { meta: {} } },
243
245
  column: {
244
246
  columnDef: columns[1],
245
247
  },
@@ -251,6 +253,7 @@ describe("generateColumns", () => {
251
253
  // "name" is a string column — should remain left-aligned
252
254
  // oxlint-disable-next-line typescript/no-explicit-any
253
255
  const nameCell = (columns[0].cell as any)({
256
+ table: { options: { meta: {} } },
254
257
  column: {
255
258
  columnDef: columns[0],
256
259
  },
@@ -271,6 +274,7 @@ describe("generateColumns", () => {
271
274
  // "age" is numeric but explicitly set to left
272
275
  // oxlint-disable-next-line typescript/no-explicit-any
273
276
  const cell = (columns[1].cell as any)({
277
+ table: { options: { meta: {} } },
274
278
  column: {
275
279
  columnDef: columns[1],
276
280
  },
@@ -311,6 +315,7 @@ describe("generateColumns", () => {
311
315
  // Assuming getCellStyleClass is a function that returns a class name
312
316
  // oxlint-disable-next-line typescript/no-explicit-any
313
317
  const cell = (columns[0].cell as any)({
318
+ table: { options: { meta: {} } },
314
319
  column: {
315
320
  columnDef: columns[0],
316
321
  },
@@ -1359,3 +1364,103 @@ test("column_widths sets fixed size and meta.width", () => {
1359
1364
  expect(b?.size).toBeUndefined();
1360
1365
  expect(b?.meta?.width).toBeUndefined();
1361
1366
  });
1367
+
1368
+ describe("renderCellValue with highlightPattern", () => {
1369
+ const createColumn = (
1370
+ dataType: "string" | "number" | "boolean" | "date",
1371
+ dtype = "object",
1372
+ ) =>
1373
+ ({
1374
+ id: "col",
1375
+ columnDef: { meta: { dataType, dtype } },
1376
+ getColumnFormatting: () => undefined,
1377
+ getColumnWrapping: () => undefined,
1378
+ applyColumnFormatting: (value: unknown) => value,
1379
+ }) as unknown as Column<unknown>;
1380
+
1381
+ const renderCell = (
1382
+ column: Column<unknown>,
1383
+ value: unknown,
1384
+ highlightQuery: string,
1385
+ ) =>
1386
+ render(
1387
+ <I18nProvider locale="en-US">
1388
+ <TooltipProvider>
1389
+ {renderCellValue({
1390
+ column,
1391
+ renderValue: () => value,
1392
+ getValue: () => value,
1393
+ selectCell: undefined,
1394
+ cellStyles: "",
1395
+ highlightPattern: buildHighlightPattern(highlightQuery),
1396
+ })}
1397
+ </TooltipProvider>
1398
+ </I18nProvider>,
1399
+ );
1400
+
1401
+ const markTexts = (container: HTMLElement) =>
1402
+ [...container.querySelectorAll("mark")].map((m) => m.textContent);
1403
+
1404
+ it("marks a match in a string cell", () => {
1405
+ const { container } = renderCell(
1406
+ createColumn("string"),
1407
+ "Alice Smith",
1408
+ "smi",
1409
+ );
1410
+ expect(markTexts(container)).toEqual(["Smi"]);
1411
+ });
1412
+
1413
+ it("marks only the middle text when the string has edge whitespace", () => {
1414
+ const { container } = renderCell(createColumn("string"), " a b ", " ");
1415
+ expect(markTexts(container)).toEqual([" "]);
1416
+ const edgeMarkers = container.querySelectorAll("span[aria-label]");
1417
+ expect(edgeMarkers).toHaveLength(2);
1418
+ for (const marker of edgeMarkers) {
1419
+ expect(marker.querySelector("mark")).toBeNull();
1420
+ }
1421
+ });
1422
+
1423
+ it("marks a match in the locale display of a number", () => {
1424
+ const { container } = renderCell(createColumn("number"), 1234.5, "234");
1425
+ expect(markTexts(container)).toEqual(["234"]);
1426
+ expect(container.textContent).toBe("1,234.5");
1427
+ });
1428
+
1429
+ it("does not mark a number when the locale display hides the raw match", () => {
1430
+ const { container } = renderCell(createColumn("number"), 1234, "1234");
1431
+ expect(markTexts(container)).toEqual([]);
1432
+ expect(container.textContent).toBe("1,234");
1433
+ });
1434
+
1435
+ it("marks a boolean cell", () => {
1436
+ const { container } = renderCell(createColumn("boolean"), false, "false");
1437
+ expect(markTexts(container)).toEqual(["False"]);
1438
+ });
1439
+
1440
+ it("marks a date cell by its display string", () => {
1441
+ const { container } = renderCell(
1442
+ createColumn("date", "date"),
1443
+ new Date(2026, 0, 15),
1444
+ "2026",
1445
+ );
1446
+ expect(markTexts(container)).toEqual(["2026"]);
1447
+ });
1448
+
1449
+ it("renders no mark for an empty query", () => {
1450
+ for (const [column, value] of [
1451
+ [createColumn("string"), "Alice"],
1452
+ [createColumn("number"), 12],
1453
+ [createColumn("boolean"), false],
1454
+ [createColumn("date", "date"), new Date(2026, 0, 15)],
1455
+ ] as const) {
1456
+ const { container } = renderCell(column, value, "");
1457
+ expect(markTexts(container)).toEqual([]);
1458
+ }
1459
+ });
1460
+
1461
+ it("renders no mark in the trigger of a long-string popout", () => {
1462
+ const value = "x".repeat(60);
1463
+ const { container } = renderCell(createColumn("string"), value, "xxx");
1464
+ expect(markTexts(container)).toEqual([]);
1465
+ });
1466
+ });
@@ -0,0 +1,109 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { render } from "@testing-library/react";
3
+ import { describe, expect, it } from "vitest";
4
+ import {
5
+ buildHighlightPattern,
6
+ findMatchRanges,
7
+ highlightMatches,
8
+ highlightRanges,
9
+ } from "../search-highlight";
10
+
11
+ function highlight(text: string, query: string) {
12
+ return highlightMatches(text, buildHighlightPattern(query));
13
+ }
14
+
15
+ function marksOf(node: React.ReactNode) {
16
+ const { container } = render(<span>{node}</span>);
17
+ return {
18
+ container,
19
+ marks: [...container.querySelectorAll("mark")].map((m) => m.textContent),
20
+ };
21
+ }
22
+
23
+ describe("buildHighlightPattern", () => {
24
+ it("returns undefined for an empty or missing query", () => {
25
+ expect(buildHighlightPattern("")).toBeUndefined();
26
+ expect(buildHighlightPattern(undefined)).toBeUndefined();
27
+ });
28
+
29
+ it("escapes regex metacharacters so the query matches literally", () => {
30
+ const { marks } = marksOf(highlight("a.b a-b (x)", ".b"));
31
+ expect(marks).toEqual([".b"]);
32
+ expect(marksOf(highlight("f(x)", "(x)")).marks).toEqual(["(x)"]);
33
+ expect(marksOf(highlight("C and C++", "c++")).marks).toEqual(["C++"]);
34
+ });
35
+ });
36
+
37
+ describe("highlightMatches", () => {
38
+ it("returns the input unchanged without a pattern", () => {
39
+ expect(highlightMatches("Alice Smith", undefined)).toBe("Alice Smith");
40
+ });
41
+
42
+ it("returns the input unchanged when there is no match", () => {
43
+ expect(highlight("Alice Smith", "zzz")).toBe("Alice Smith");
44
+ });
45
+
46
+ it("returns the input unchanged when the query is longer than the text", () => {
47
+ expect(highlight("ab", "abc")).toBe("ab");
48
+ });
49
+
50
+ it("wraps a single match and keeps the surrounding text", () => {
51
+ const { container, marks } = marksOf(highlight("Alice Smith", "ce S"));
52
+ expect(marks).toEqual(["ce S"]);
53
+ expect(container.textContent).toBe("Alice Smith");
54
+ });
55
+
56
+ it("matches case-insensitively and keeps the original casing", () => {
57
+ const { marks } = marksOf(highlight("Alice Smith", "smi"));
58
+ expect(marks).toEqual(["Smi"]);
59
+ });
60
+
61
+ it("wraps every occurrence", () => {
62
+ const { container, marks } = marksOf(highlight("a-A-a", "a"));
63
+ expect(marks).toEqual(["a", "A", "a"]);
64
+ expect(container.textContent).toBe("a-A-a");
65
+ });
66
+
67
+ it("produces no empty segments for matches at the edges", () => {
68
+ const segments = highlight("xax", "x") as React.ReactNode[];
69
+ expect(Array.isArray(segments)).toBe(true);
70
+ expect(segments).toHaveLength(3);
71
+ expect(segments[1]).toBe("a");
72
+ });
73
+
74
+ it("is safe to reuse one global pattern across calls", () => {
75
+ const pattern = buildHighlightPattern("a");
76
+ expect(marksOf(highlightMatches("aa", pattern)).marks).toEqual(["a", "a"]);
77
+ expect(marksOf(highlightMatches("aa", pattern)).marks).toEqual(["a", "a"]);
78
+ });
79
+ });
80
+
81
+ describe("Unicode case folding", () => {
82
+ it("folds the Kelvin sign to k like the backend engines", () => {
83
+ expect(marksOf(highlight("K", "k")).marks).toEqual(["K"]);
84
+ });
85
+
86
+ it("folds every Greek sigma form", () => {
87
+ expect(marksOf(highlight("ΟΣ ος οσ", "σ")).marks).toEqual(["Σ", "ς", "σ"]);
88
+ });
89
+ });
90
+
91
+ describe("highlightRanges", () => {
92
+ it("applies shared ranges to a segment at an offset", () => {
93
+ const ranges = findMatchRanges(
94
+ "see https://x",
95
+ buildHighlightPattern("see https"),
96
+ );
97
+ expect(ranges).toEqual([[0, 9]]);
98
+ const { marks: textMarks } = marksOf(highlightRanges("see ", ranges, 0));
99
+ expect(textMarks).toEqual(["see "]);
100
+ const { marks: linkMarks } = marksOf(
101
+ highlightRanges("https://x", ranges, 4),
102
+ );
103
+ expect(linkMarks).toEqual(["https"]);
104
+ });
105
+
106
+ it("returns the segment unchanged when no range overlaps it", () => {
107
+ expect(highlightRanges("zzz", [[10, 12]], 0)).toBe("zzz");
108
+ });
109
+ });
@@ -2,6 +2,7 @@
2
2
  import { fireEvent, render, screen } from "@testing-library/react";
3
3
  import { describe, expect, it, vi } from "vitest";
4
4
  import { parseContent } from "@/utils/url-parser";
5
+ import { buildHighlightPattern } from "../search-highlight";
5
6
  import { isMarkdown, UrlDetector } from "../url-detector";
6
7
 
7
8
  describe("UrlDetector", () => {
@@ -201,3 +202,76 @@ describe("isMarkdown", () => {
201
202
  expect(isMarkdown("Visit [marimo](https://marimo.io)")).toBe(true);
202
203
  });
203
204
  });
205
+
206
+ describe("UrlDetector with highlightPattern", () => {
207
+ it("wraps a text match in a mark", () => {
208
+ const { container } = render(
209
+ <UrlDetector
210
+ parts={parseContent("Alice Smith")}
211
+ highlightPattern={buildHighlightPattern("smi")}
212
+ />,
213
+ );
214
+ const marks = container.querySelectorAll("mark");
215
+ expect(marks).toHaveLength(1);
216
+ expect(marks[0].textContent).toBe("Smi");
217
+ expect(container.textContent).toBe("Alice Smith");
218
+ });
219
+
220
+ it("wraps a match inside a link label", () => {
221
+ const { container } = render(
222
+ <UrlDetector
223
+ parts={parseContent("see https://marimo.io/docs")}
224
+ highlightPattern={buildHighlightPattern("marimo")}
225
+ />,
226
+ );
227
+ const link = container.querySelector("a");
228
+ expect(link).not.toBeNull();
229
+ const mark = link?.querySelector("mark");
230
+ expect(mark?.textContent).toBe("marimo");
231
+ expect(link).toHaveAttribute("href", "https://marimo.io/docs");
232
+ });
233
+
234
+ it("renders no mark for an empty query", () => {
235
+ const { container } = render(
236
+ <UrlDetector
237
+ parts={parseContent("Alice Smith")}
238
+ highlightPattern={buildHighlightPattern("")}
239
+ />,
240
+ );
241
+ expect(container.querySelector("mark")).toBeNull();
242
+ });
243
+ });
244
+
245
+ describe("UrlDetector highlight across part boundaries", () => {
246
+ it("marks a query that spans a text part and a link", () => {
247
+ const { container } = render(
248
+ <UrlDetector
249
+ parts={parseContent("see https://marimo.io/docs")}
250
+ highlightPattern={buildHighlightPattern("see https")}
251
+ />,
252
+ );
253
+ const marks = [...container.querySelectorAll("mark")].map(
254
+ (m) => m.textContent,
255
+ );
256
+ expect(marks).toEqual(["see ", "https"]);
257
+ const link = container.querySelector("a");
258
+ expect(link?.querySelector("mark")?.textContent).toBe("https");
259
+ expect(link).toHaveAttribute("href", "https://marimo.io/docs");
260
+ expect(container.textContent).toBe("see https://marimo.io/docs");
261
+ });
262
+
263
+ it("marks the fallback link when an image fails to load", () => {
264
+ const { container } = render(
265
+ <UrlDetector
266
+ parts={parseContent("https://example.com/image.png")}
267
+ highlightPattern={buildHighlightPattern("image")}
268
+ />,
269
+ );
270
+ const img = container.querySelector("img");
271
+ expect(img).not.toBeNull();
272
+ fireEvent.error(img as HTMLImageElement);
273
+ const link = container.querySelector("a");
274
+ expect(link).toHaveAttribute("href", "https://example.com/image.png");
275
+ expect(link?.querySelector("mark")?.textContent).toBe("image");
276
+ });
277
+ });
@@ -41,6 +41,7 @@ import {
41
41
  isNumericType,
42
42
  } from "./types";
43
43
  import { SentinelCell, WhitespaceMarkers } from "./sentinel-cell";
44
+ import { highlightMatches } from "./search-highlight";
44
45
  import { detectSentinel, splitLeadingTrailingWhitespace } from "./utils";
45
46
  import { uniformSample } from "./uniformSample";
46
47
  import { MarkdownUrlDetector, UrlDetector } from "./url-detector";
@@ -278,7 +279,7 @@ export function generateColumns<T>({
278
279
  );
279
280
  },
280
281
 
281
- cell: ({ column, renderValue, getValue, cell }) => {
282
+ cell: ({ column, renderValue, getValue, cell, table }) => {
282
283
  function selectCell() {
283
284
  if (selection !== "single-cell" && selection !== "multi-cell") {
284
285
  return;
@@ -310,6 +311,7 @@ export function generateColumns<T>({
310
311
  getValue,
311
312
  selectCell,
312
313
  cellStyles,
314
+ highlightPattern: table.options.meta?.highlightPattern,
313
315
  });
314
316
 
315
317
  // Row headers are bold
@@ -586,12 +588,14 @@ function renderDate({
586
588
  dtype,
587
589
  displayValue,
588
590
  locale,
591
+ highlightPattern,
589
592
  }: {
590
593
  value: Date;
591
594
  dataType?: DataType;
592
595
  dtype?: string;
593
596
  displayValue?: string;
594
597
  locale: string;
598
+ highlightPattern?: RegExp;
595
599
  }): React.ReactNode {
596
600
  const type = dataType === "date" ? "date" : "datetime";
597
601
  const timezone = extractTimezone(dtype);
@@ -600,7 +604,7 @@ function renderDate({
600
604
 
601
605
  return (
602
606
  <DatePopover date={value} type={type}>
603
- {exactValue}
607
+ {highlightMatches(exactValue, highlightPattern)}
604
608
  </DatePopover>
605
609
  );
606
610
  }
@@ -611,12 +615,14 @@ export function renderCellValue<TData, TValue>({
611
615
  getValue,
612
616
  selectCell,
613
617
  cellStyles,
618
+ highlightPattern,
614
619
  }: {
615
620
  column: Column<TData, TValue>;
616
621
  renderValue: () => TValue | null;
617
622
  getValue: () => TValue;
618
623
  selectCell?: () => void;
619
624
  cellStyles?: string;
625
+ highlightPattern?: RegExp;
620
626
  }) {
621
627
  const value = getValue();
622
628
  const format = column.getColumnFormatting?.();
@@ -626,6 +632,28 @@ export function renderCellValue<TData, TValue>({
626
632
 
627
633
  const isWrapped = column.getColumnWrapping?.() === "wrap";
628
634
 
635
+ const textCell = (text: string) => (
636
+ <div onClick={selectCell} className={cellStyles}>
637
+ {highlightMatches(text, highlightPattern)}
638
+ </div>
639
+ );
640
+ const dateCell = (date: Date, displayValue?: string) => (
641
+ <div onClick={selectCell} className={cellStyles}>
642
+ <WithLocale>
643
+ {(locale) =>
644
+ renderDate({
645
+ value: date,
646
+ dataType,
647
+ dtype,
648
+ displayValue,
649
+ locale,
650
+ highlightPattern,
651
+ })
652
+ }
653
+ </WithLocale>
654
+ </div>
655
+ );
656
+
629
657
  // Sentinel values (null, whitespace, NaN, Infinity, NaT) rendered specially.
630
658
  // Empty strings are left as-is
631
659
  const sentinel = detectSentinel(value, dataType, dtype);
@@ -641,23 +669,10 @@ export function renderCellValue<TData, TValue>({
641
669
  try {
642
670
  const date = new Date(value);
643
671
  if (!format || !isValid(date)) {
644
- return (
645
- <div onClick={selectCell} className={cellStyles}>
646
- {value}
647
- </div>
648
- );
672
+ return textCell(value);
649
673
  }
650
674
 
651
- const displayValue = String(column.applyColumnFormatting(value));
652
- return (
653
- <div onClick={selectCell} className={cellStyles}>
654
- <WithLocale>
655
- {(locale) =>
656
- renderDate({ value: date, dataType, dtype, displayValue, locale })
657
- }
658
- </WithLocale>
659
- </div>
660
- );
675
+ return dateCell(date, String(column.applyColumnFormatting(value)));
661
676
  } catch (error) {
662
677
  Logger.error("Error parsing datetime, fallback to string", error);
663
678
  }
@@ -673,17 +688,9 @@ export function renderCellValue<TData, TValue>({
673
688
  }
674
689
 
675
690
  // e.g. 2010-10-07 17:15:00
676
- const displayValue = format
677
- ? String(column.applyColumnFormatting(value))
678
- : undefined;
679
- return (
680
- <div onClick={selectCell} className={cellStyles}>
681
- <WithLocale>
682
- {(locale) =>
683
- renderDate({ value, dataType, dtype, displayValue, locale })
684
- }
685
- </WithLocale>
686
- </div>
691
+ return dateCell(
692
+ value,
693
+ format ? String(column.applyColumnFormatting(value)) : undefined,
687
694
  );
688
695
  }
689
696
 
@@ -729,7 +736,7 @@ export function renderCellValue<TData, TValue>({
729
736
  className={cn(cellStyles, isWrapped && COLUMN_WRAPPING_STYLES)}
730
737
  >
731
738
  <WhitespaceMarkers value={leading} />
732
- <UrlDetector parts={parts} />
739
+ <UrlDetector parts={parts} highlightPattern={highlightPattern} />
733
740
  <WhitespaceMarkers value={trailing} />
734
741
  </div>
735
742
  );
@@ -755,11 +762,7 @@ export function renderCellValue<TData, TValue>({
755
762
  }
756
763
 
757
764
  if (format) {
758
- return (
759
- <div onClick={selectCell} className={cellStyles}>
760
- {column.applyColumnFormatting(value)}
761
- </div>
762
- );
765
+ return textCell(String(column.applyColumnFormatting(value)));
763
766
  }
764
767
 
765
768
  // Format to the correct locale
@@ -769,26 +772,19 @@ export function renderCellValue<TData, TValue>({
769
772
  <LocaleNumber
770
773
  value={value}
771
774
  minFractionDigits={column.columnDef.meta?.minFractionDigits}
775
+ highlightPattern={highlightPattern}
772
776
  />
773
777
  </div>
774
778
  );
775
779
  }
776
780
 
777
781
  if (typeof value === "boolean") {
778
- return (
779
- <div onClick={selectCell} className={cellStyles}>
780
- {value ? "True" : "False"}
781
- </div>
782
- );
782
+ return textCell(value ? "True" : "False");
783
783
  }
784
784
 
785
785
  if (isPrimitiveOrNullish(value)) {
786
786
  const rendered = renderValue();
787
- return (
788
- <div onClick={selectCell} className={cellStyles}>
789
- {rendered == null ? "" : String(rendered)}
790
- </div>
791
- );
787
+ return textCell(rendered == null ? "" : String(rendered));
792
788
  }
793
789
 
794
790
  const mimeValues = getMimeValues(value);
@@ -826,14 +822,16 @@ export function renderCellValue<TData, TValue>({
826
822
  export const LocaleNumber = ({
827
823
  value,
828
824
  minFractionDigits,
825
+ highlightPattern,
829
826
  }: {
830
827
  value: number;
831
828
  minFractionDigits?: number;
829
+ highlightPattern?: RegExp;
832
830
  }) => {
833
831
  const { locale } = useLocale();
834
832
  const format = useNumberFormatter({
835
833
  minimumFractionDigits: minFractionDigits,
836
834
  maximumFractionDigits: maxFractionalDigits(locale),
837
835
  });
838
- return format.format(value);
836
+ return highlightMatches(format.format(value), highlightPattern);
839
837
  };
@@ -58,6 +58,7 @@ import { useColumnPinning } from "./hooks/use-column-pinning";
58
58
  import { useScrollContainerHeight } from "./hooks/use-scroll-container-height";
59
59
  import { CellSelectionProvider } from "./range-focus/provider";
60
60
  import { DataTableBody, renderTableHeader } from "./renderers";
61
+ import { buildHighlightPattern } from "./search-highlight";
61
62
  import { TableBottomBar } from "./TableBottomBar";
62
63
  import { TableTopBar } from "./TableTopBar";
63
64
  import {
@@ -231,6 +232,11 @@ const DataTableInternal = <TData,>({
231
232
  return idx + offset;
232
233
  }
233
234
 
235
+ const highlightPattern = React.useMemo(
236
+ () => buildHighlightPattern(searchQuery),
237
+ [searchQuery],
238
+ );
239
+
234
240
  const table = useReactTable<TData>({
235
241
  _features: [
236
242
  ColumnPinning,
@@ -245,7 +251,7 @@ const DataTableInternal = <TData,>({
245
251
  ],
246
252
  data,
247
253
  columns,
248
- meta: { rawData },
254
+ meta: { rawData, highlightPattern },
249
255
  getCoreRowModel: getCoreRowModel(),
250
256
  // pagination
251
257
  rowCount: totalRows === "too_many" ? undefined : totalRows,