@marimo-team/islands 0.21.2-dev78 → 0.21.2-dev79

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 (71) hide show
  1. package/dist/{any-language-editor-CSvBdMtG.js → any-language-editor-BBegsg-m.js} +17 -17
  2. package/dist/{chat-ui-CedqKBe3.js → chat-ui-D4ay_SFE.js} +9 -9
  3. package/dist/{dist-CEmSeYaL.js → dist-1-E4bC9V.js} +2 -2
  4. package/dist/{dist-DoO66t8e.js → dist-APyhcmvq.js} +1 -1
  5. package/dist/{dist-B6ZvKwIz.js → dist-B5thW2rT.js} +1 -1
  6. package/dist/{dist-mpvRA2Bc.js → dist-BFe8Nc_2.js} +3 -3
  7. package/dist/dist-BHGf2Zey.js +5 -0
  8. package/dist/dist-BImOGJZM.js +5 -0
  9. package/dist/{dist-DKr1M-ru.js → dist-BJvk9DSp.js} +1 -1
  10. package/dist/{dist-fRW2mZtS.js → dist-BLrc9iNb.js} +2 -2
  11. package/dist/{dist-DqweMfpO.js → dist-BYr4a-wE.js} +1 -1
  12. package/dist/dist-BbJfmCHJ.js +5 -0
  13. package/dist/{dist-Bc-mOFlk.js → dist-BrJQDkv_.js} +2 -2
  14. package/dist/dist-C0v-nFs_.js +8 -0
  15. package/dist/{dist-D2nC1On3.js → dist-C16JG-Ok.js} +2 -2
  16. package/dist/{dist-N1lm7LVK.js → dist-C4EV3aDt.js} +37 -71
  17. package/dist/dist-C8N7114Z.js +5 -0
  18. package/dist/dist-C9TDg2aq.js +8 -0
  19. package/dist/{dist-DvjVNme1.js → dist-CGH6Jw-c.js} +1 -1
  20. package/dist/{dist-CXsGjNbR.js → dist-CKVkWFcB.js} +2 -2
  21. package/dist/dist-CN4RKgNR.js +5 -0
  22. package/dist/{dist--MUqhsBh.js → dist-CQ3XmGIH.js} +1 -1
  23. package/dist/{dist-B771Ewvn.js → dist-CSiTW5NO.js} +3 -3
  24. package/dist/dist-CVOkf_3P.js +6 -0
  25. package/dist/{dist-8-jPFKws.js → dist-CYuoqvce.js} +2 -2
  26. package/dist/dist-Cbkga3s5.js +5 -0
  27. package/dist/{dist-TuFHGVp2.js → dist-Cm4SOB1A.js} +1 -1
  28. package/dist/{dist-BsyHyGqX.js → dist-CxczluAk.js} +2 -2
  29. package/dist/dist-D6YTv0Kj.js +5 -0
  30. package/dist/{dist-DlacJSaE.js → dist-D75XqRaT.js} +2 -2
  31. package/dist/dist-D9vawryf.js +8 -0
  32. package/dist/{dist-CpNdP46s.js → dist-DFBjYgbq.js} +2 -2
  33. package/dist/{dist-DqT0O-4m.js → dist-DUg5n_x5.js} +5 -5
  34. package/dist/dist-DasJgTvL.js +6 -0
  35. package/dist/{dist-DNSJBJI7.js → dist-DbyoYfBn.js} +2 -2
  36. package/dist/{dist-CsToCMcA.js → dist-Dd9vDQtp.js} +2 -2
  37. package/dist/{dist-QxBlbqgj.js → dist-Dk13KZ54.js} +3 -3
  38. package/dist/dist-Ui51qXMz.js +8 -0
  39. package/dist/{dist-DEmPLkH9.js → dist-XYBhoeSX.js} +4 -4
  40. package/dist/{dist-CwP9oyR8.js → dist-Y9GfSR6_.js} +2 -2
  41. package/dist/{dist-BjYGwQ65.js → dist-aW74oyUt.js} +1 -1
  42. package/dist/dist-d2msfN-B.js +5 -0
  43. package/dist/{dist-Bg1YGq2b.js → dist-iM9VwH8Z.js} +1 -1
  44. package/dist/{dist-DSSPn3fP.js → dist-uR-o9IVx.js} +2 -2
  45. package/dist/{dist-CJEfzjG5.js → dist-zGOEySUQ.js} +4 -4
  46. package/dist/{esm-B_4hpmV_.js → esm-CCD9xN05.js} +22 -22
  47. package/dist/{esm-Bcbo0Yvz.js → esm-CxoKu9RN.js} +3 -3
  48. package/dist/main.js +948 -333
  49. package/dist/{process-output-Xg5O7mVz.js → process-output-Cv8vQ4At.js} +33 -33
  50. package/package.json +2 -1
  51. package/src/__tests__/setup.ts +15 -0
  52. package/src/components/data-table/TableActions.tsx +8 -2
  53. package/src/components/data-table/__tests__/data-table.test.tsx +63 -0
  54. package/src/components/data-table/data-table.tsx +10 -31
  55. package/src/components/data-table/hooks/use-scroll-container-height.ts +97 -0
  56. package/src/components/data-table/renderers.tsx +103 -46
  57. package/src/components/data-table/types.ts +14 -0
  58. package/dist/dist-BK8Ch8me.js +0 -5
  59. package/dist/dist-BPlAe3w3.js +0 -6
  60. package/dist/dist-BQnLGf1A.js +0 -8
  61. package/dist/dist-BZRBnAnM.js +0 -5
  62. package/dist/dist-BmTKj-5m.js +0 -5
  63. package/dist/dist-C1UyhB0b.js +0 -5
  64. package/dist/dist-C1uqqTuH.js +0 -5
  65. package/dist/dist-CCeSY9xv.js +0 -5
  66. package/dist/dist-CMWhb0s1.js +0 -5
  67. package/dist/dist-DYoFz28E.js +0 -8
  68. package/dist/dist-DeSnQwvL.js +0 -6
  69. package/dist/dist-e8VqCQ_P.js +0 -5
  70. package/dist/dist-kGaha3sX.js +0 -8
  71. package/dist/dist-wQD8nhf9.js +0 -8
@@ -17,19 +17,19 @@ import { H as string, L as number, N as literal, O as array, R as object, W as u
17
17
  import { t as require_jsx_runtime } from "./jsx-runtime-9hcJiI23.js";
18
18
  import { t as require_react_dom } from "./react-dom-BKwCWYPW.js";
19
19
  import { i as TooltipProvider, t as Tooltip } from "./tooltip-DnI4CwIS.js";
20
- import { a as linter, c as historyField, i as forEachDiagnostic, l as insertTab, o as setDiagnostics, s as history, u as CopyClipboardIcon } from "./esm-Bcbo0Yvz.js";
20
+ import { a as linter, c as historyField, i as forEachDiagnostic, l as insertTab, o as setDiagnostics, s as history, u as CopyClipboardIcon } from "./esm-CxoKu9RN.js";
21
21
  import { t as toInteger_default } from "./toInteger-BUeg_O0F.js";
22
22
  import { i as debounce_default } from "./constants-LmeAawHa.js";
23
23
  import { a as getResolvedMarimoConfig, g as useAtomValue, h as useAtom, i as autoInstantiateAtom, l as createDeepEqualAtom, m as Provider, n as useTheme, p as isIslands, t as resolvedThemeAtom, u as store, w as isEqual_default, y as atom } from "./useTheme-5GTtjXjy.js";
24
- import { $ as ViewPlugin, At as Prec, B as tags, Dt as EditorState, E as getIndentUnit, Et as EditorSelection, Ft as StateField, I as syntaxTree, It as Text, J as parseMixed, L as unfoldAll, Lt as Transaction, Ot as Facet, Pt as StateEffect, Q as GutterMarker, S as foldInside, Tt as Compartment, Y as Decoration, Z as EditorView, b as foldAll, dt as keymap, f as StreamLanguage, jt as RangeSet, l as LanguageDescription, mt as placeholder, ot as gutter, u as LanguageSupport, ut as hoverTooltip, vt as showPanel, w as foldNodeProp, xt as Annotation, yt as showTooltip } from "./dist-N1lm7LVK.js";
24
+ import { $ as ViewPlugin, At as Prec, B as tags, Dt as EditorState, E as getIndentUnit, Et as EditorSelection, Ft as StateField, I as syntaxTree, It as Text, J as parseMixed, L as unfoldAll, Lt as Transaction, Ot as Facet, Pt as StateEffect, Q as GutterMarker, S as foldInside, Tt as Compartment, Y as Decoration, Z as EditorView, b as foldAll, dt as keymap, f as StreamLanguage, jt as RangeSet, l as LanguageDescription, mt as placeholder, ot as gutter, u as LanguageSupport, ut as hoverTooltip, vt as showPanel, w as foldNodeProp, xt as Annotation, yt as showTooltip } from "./dist-C4EV3aDt.js";
25
25
  import { t as invariant } from "./invariant-Ctm_8TNZ.js";
26
26
  import { a as arrayInsert, i as arrayDelete, n as once, o as arrayInsertMany, s as arrayMove, u as clamp } from "./once-DjP4Kbhy.js";
27
27
  import { t as getIframeCapabilities } from "./capabilities-DAGZLwa6.js";
28
- import { d as snippet, n as acceptCompletion, r as autocompletion, u as insertCompletionText } from "./dist-DqweMfpO.js";
28
+ import { d as snippet, n as acceptCompletion, r as autocompletion, u as insertCompletionText } from "./dist-BYr4a-wE.js";
29
29
  import { t as require_main } from "./main-sxFlUO_N.js";
30
- import { a as PLSQL, c as SQLite, d as schemaCompletionSource, f as sql, i as MySQL, l as StandardSQL, n as MSSQL, o as PostgreSQL, r as MariaSQL, s as SQLDialect, t as Cassandra, u as keywordCompletionSource } from "./dist-DlacJSaE.js";
31
- import { a as markdown, s as markdownLanguage } from "./dist-B771Ewvn.js";
32
- import { a as parser, i as pythonLanguage, n as localCompletionSource, r as python, t as globalCompletion } from "./dist-D2nC1On3.js";
30
+ import { a as PLSQL, c as SQLite, d as schemaCompletionSource, f as sql, i as MySQL, l as StandardSQL, n as MSSQL, o as PostgreSQL, r as MariaSQL, s as SQLDialect, t as Cassandra, u as keywordCompletionSource } from "./dist-D75XqRaT.js";
31
+ import { a as markdown, s as markdownLanguage } from "./dist-CSiTW5NO.js";
32
+ import { a as parser, i as pythonLanguage, n as localCompletionSource, r as python, t as globalCompletion } from "./dist-C16JG-Ok.js";
33
33
  import { n as stexMath } from "./stex-7yEw16Ww.js";
34
34
  import { t as purify } from "./purify.es-Co_dANLh.js";
35
35
  import { t as useAsyncData } from "./useAsyncData-BtHYXgXF.js";
@@ -16172,7 +16172,7 @@ ${r.join("\n")}`;
16172
16172
  return new LanguageSupport(StreamLanguage.define(e));
16173
16173
  }
16174
16174
  function sql$1(e) {
16175
- return import("./dist-CsToCMcA.js").then((t) => t.sql({
16175
+ return import("./dist-Dd9vDQtp.js").then((t) => t.sql({
16176
16176
  dialect: t[e]
16177
16177
  }));
16178
16178
  }
@@ -16185,7 +16185,7 @@ ${r.join("\n")}`;
16185
16185
  "ino"
16186
16186
  ],
16187
16187
  load() {
16188
- return import("./dist-C1uqqTuH.js").then((e) => e.cpp());
16188
+ return import("./dist-D6YTv0Kj.js").then((e) => e.cpp());
16189
16189
  }
16190
16190
  }),
16191
16191
  LanguageDescription.of({
@@ -16204,7 +16204,7 @@ ${r.join("\n")}`;
16204
16204
  "hxx"
16205
16205
  ],
16206
16206
  load() {
16207
- return import("./dist-C1uqqTuH.js").then((e) => e.cpp());
16207
+ return import("./dist-D6YTv0Kj.js").then((e) => e.cpp());
16208
16208
  }
16209
16209
  }),
16210
16210
  LanguageDescription.of({
@@ -16225,7 +16225,7 @@ ${r.join("\n")}`;
16225
16225
  "css"
16226
16226
  ],
16227
16227
  load() {
16228
- return import("./dist-CMWhb0s1.js").then((e) => e.css());
16228
+ return import("./dist-Cbkga3s5.js").then((e) => e.css());
16229
16229
  }
16230
16230
  }),
16231
16231
  LanguageDescription.of({
@@ -16234,7 +16234,7 @@ ${r.join("\n")}`;
16234
16234
  "go"
16235
16235
  ],
16236
16236
  load() {
16237
- return import("./dist-CXsGjNbR.js").then((e) => e.go());
16237
+ return import("./dist-CKVkWFcB.js").then((e) => e.go());
16238
16238
  }
16239
16239
  }),
16240
16240
  LanguageDescription.of({
@@ -16249,7 +16249,7 @@ ${r.join("\n")}`;
16249
16249
  "hbs"
16250
16250
  ],
16251
16251
  load() {
16252
- return import("./dist-DEmPLkH9.js").then((e) => e.html());
16252
+ return import("./dist-XYBhoeSX.js").then((e) => e.html());
16253
16253
  }
16254
16254
  }),
16255
16255
  LanguageDescription.of({
@@ -16258,7 +16258,7 @@ ${r.join("\n")}`;
16258
16258
  "java"
16259
16259
  ],
16260
16260
  load() {
16261
- return import("./dist-e8VqCQ_P.js").then((e) => e.java());
16261
+ return import("./dist-BHGf2Zey.js").then((e) => e.java());
16262
16262
  }
16263
16263
  }),
16264
16264
  LanguageDescription.of({
@@ -16274,7 +16274,7 @@ ${r.join("\n")}`;
16274
16274
  "cjs"
16275
16275
  ],
16276
16276
  load() {
16277
- return import("./dist-CwP9oyR8.js").then((e) => e.javascript());
16277
+ return import("./dist-Y9GfSR6_.js").then((e) => e.javascript());
16278
16278
  }
16279
16279
  }),
16280
16280
  LanguageDescription.of({
@@ -16285,7 +16285,7 @@ ${r.join("\n")}`;
16285
16285
  "jinja2"
16286
16286
  ],
16287
16287
  load() {
16288
- return import("./dist-wQD8nhf9.js").then((e) => e.jinja());
16288
+ return import("./dist-Ui51qXMz.js").then((e) => e.jinja());
16289
16289
  }
16290
16290
  }),
16291
16291
  LanguageDescription.of({
@@ -16298,7 +16298,7 @@ ${r.join("\n")}`;
16298
16298
  "map"
16299
16299
  ],
16300
16300
  load() {
16301
- return import("./dist-C1UyhB0b.js").then((e) => e.json());
16301
+ return import("./dist-CN4RKgNR.js").then((e) => e.json());
16302
16302
  }
16303
16303
  }),
16304
16304
  LanguageDescription.of({
@@ -16307,7 +16307,7 @@ ${r.join("\n")}`;
16307
16307
  "jsx"
16308
16308
  ],
16309
16309
  load() {
16310
- return import("./dist-CwP9oyR8.js").then((e) => e.javascript({
16310
+ return import("./dist-Y9GfSR6_.js").then((e) => e.javascript({
16311
16311
  jsx: true
16312
16312
  }));
16313
16313
  }
@@ -16318,7 +16318,7 @@ ${r.join("\n")}`;
16318
16318
  "less"
16319
16319
  ],
16320
16320
  load() {
16321
- return import("./dist-BPlAe3w3.js").then((e) => e.less());
16321
+ return import("./dist-CVOkf_3P.js").then((e) => e.less());
16322
16322
  }
16323
16323
  }),
16324
16324
  LanguageDescription.of({
@@ -16327,7 +16327,7 @@ ${r.join("\n")}`;
16327
16327
  "liquid"
16328
16328
  ],
16329
16329
  load() {
16330
- return import("./dist-DYoFz28E.js").then((e) => e.liquid());
16330
+ return import("./dist-C9TDg2aq.js").then((e) => e.liquid());
16331
16331
  }
16332
16332
  }),
16333
16333
  LanguageDescription.of({
@@ -16344,7 +16344,7 @@ ${r.join("\n")}`;
16344
16344
  "mkd"
16345
16345
  ],
16346
16346
  load() {
16347
- return import("./dist-DqT0O-4m.js").then((e) => e.markdown());
16347
+ return import("./dist-DUg5n_x5.js").then((e) => e.markdown());
16348
16348
  }
16349
16349
  }),
16350
16350
  LanguageDescription.of({
@@ -16370,7 +16370,7 @@ ${r.join("\n")}`;
16370
16370
  "phtml"
16371
16371
  ],
16372
16372
  load() {
16373
- return import("./dist-BQnLGf1A.js").then((e) => e.php());
16373
+ return import("./dist-D9vawryf.js").then((e) => e.php());
16374
16374
  }
16375
16375
  }),
16376
16376
  LanguageDescription.of({
@@ -16398,7 +16398,7 @@ ${r.join("\n")}`;
16398
16398
  ],
16399
16399
  filename: /^(BUCK|BUILD)$/,
16400
16400
  load() {
16401
- return import("./dist-BmTKj-5m.js").then((e) => e.python());
16401
+ return import("./dist-C8N7114Z.js").then((e) => e.python());
16402
16402
  }
16403
16403
  }),
16404
16404
  LanguageDescription.of({
@@ -16407,7 +16407,7 @@ ${r.join("\n")}`;
16407
16407
  "rs"
16408
16408
  ],
16409
16409
  load() {
16410
- return import("./dist-CCeSY9xv.js").then((e) => e.rust());
16410
+ return import("./dist-BImOGJZM.js").then((e) => e.rust());
16411
16411
  }
16412
16412
  }),
16413
16413
  LanguageDescription.of({
@@ -16416,7 +16416,7 @@ ${r.join("\n")}`;
16416
16416
  "sass"
16417
16417
  ],
16418
16418
  load() {
16419
- return import("./dist-DeSnQwvL.js").then((e) => e.sass({
16419
+ return import("./dist-DasJgTvL.js").then((e) => e.sass({
16420
16420
  indented: true
16421
16421
  }));
16422
16422
  }
@@ -16427,7 +16427,7 @@ ${r.join("\n")}`;
16427
16427
  "scss"
16428
16428
  ],
16429
16429
  load() {
16430
- return import("./dist-DeSnQwvL.js").then((e) => e.sass());
16430
+ return import("./dist-DasJgTvL.js").then((e) => e.sass());
16431
16431
  }
16432
16432
  }),
16433
16433
  LanguageDescription.of({
@@ -16451,7 +16451,7 @@ ${r.join("\n")}`;
16451
16451
  "tsx"
16452
16452
  ],
16453
16453
  load() {
16454
- return import("./dist-CwP9oyR8.js").then((e) => e.javascript({
16454
+ return import("./dist-Y9GfSR6_.js").then((e) => e.javascript({
16455
16455
  jsx: true,
16456
16456
  typescript: true
16457
16457
  }));
@@ -16468,7 +16468,7 @@ ${r.join("\n")}`;
16468
16468
  "cts"
16469
16469
  ],
16470
16470
  load() {
16471
- return import("./dist-CwP9oyR8.js").then((e) => e.javascript({
16471
+ return import("./dist-Y9GfSR6_.js").then((e) => e.javascript({
16472
16472
  typescript: true
16473
16473
  }));
16474
16474
  }
@@ -16480,7 +16480,7 @@ ${r.join("\n")}`;
16480
16480
  "wast"
16481
16481
  ],
16482
16482
  load() {
16483
- return import("./dist-BZRBnAnM.js").then((e) => e.wast());
16483
+ return import("./dist-d2msfN-B.js").then((e) => e.wast());
16484
16484
  }
16485
16485
  }),
16486
16486
  LanguageDescription.of({
@@ -16497,7 +16497,7 @@ ${r.join("\n")}`;
16497
16497
  "svg"
16498
16498
  ],
16499
16499
  load() {
16500
- return import("./dist-DSSPn3fP.js").then((e) => e.xml());
16500
+ return import("./dist-uR-o9IVx.js").then((e) => e.xml());
16501
16501
  }
16502
16502
  }),
16503
16503
  LanguageDescription.of({
@@ -16510,7 +16510,7 @@ ${r.join("\n")}`;
16510
16510
  "yml"
16511
16511
  ],
16512
16512
  load() {
16513
- return import("./dist-BK8Ch8me.js").then((e) => e.yaml());
16513
+ return import("./dist-BbJfmCHJ.js").then((e) => e.yaml());
16514
16514
  }
16515
16515
  }),
16516
16516
  LanguageDescription.of({
@@ -17610,13 +17610,13 @@ ${r.join("\n")}`;
17610
17610
  "vue"
17611
17611
  ],
17612
17612
  load() {
17613
- return import("./dist-kGaha3sX.js").then((e) => e.vue());
17613
+ return import("./dist-C0v-nFs_.js").then((e) => e.vue());
17614
17614
  }
17615
17615
  }),
17616
17616
  LanguageDescription.of({
17617
17617
  name: "Angular Template",
17618
17618
  load() {
17619
- return import("./dist-CJEfzjG5.js").then((e) => e.angular());
17619
+ return import("./dist-zGOEySUQ.js").then((e) => e.angular());
17620
17620
  }
17621
17621
  })
17622
17622
  ], cache$1 = /* @__PURE__ */ new WeakMap(), newline = /(\n|\r\n?|\u2028|\u2029)/g, leadingWhitespace = /^\s*/, nonWhitespace = /\S/, slice = Array.prototype.slice, zero = 48, nine = 57, lowerA = 97, lowerF = 102, upperA = 65, upperF = 70;
@@ -27193,7 +27193,7 @@ ${n.sqlString}
27193
27193
  hasConsoleOutput: (o == null ? void 0 : o.consoleOutputs) != null
27194
27194
  };
27195
27195
  }
27196
- LazyAnyLanguageCodeMirror = (0, import_react.lazy)(() => import("./any-language-editor-CSvBdMtG.js"));
27196
+ LazyAnyLanguageCodeMirror = (0, import_react.lazy)(() => import("./any-language-editor-BBegsg-m.js"));
27197
27197
  var import_compiler_runtime$1 = require_compiler_runtime(), extensions = [
27198
27198
  EditorView.lineWrapping
27199
27199
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.21.2-dev78",
3
+ "version": "0.21.2-dev79",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -96,6 +96,7 @@
96
96
  "@tailwindcss/postcss": "^4.1.18",
97
97
  "@tailwindcss/typography": "^0.5.19",
98
98
  "@tanstack/react-table": "^8.21.3",
99
+ "@tanstack/react-virtual": "^3.13.23",
99
100
  "@textea/json-viewer": "^4.0.1",
100
101
  "@types/humanize-duration": "^3.27.4",
101
102
  "@types/js-cookie": "^3.0.6",
@@ -5,6 +5,21 @@ import { afterEach, beforeEach, vi } from "vitest";
5
5
  import "@testing-library/jest-dom/vitest";
6
6
  import "blob-polyfill";
7
7
 
8
+ // mock implementation because jsdom doesn't support ResizeObserver
9
+ // if we need to test ResizeObserver functionality
10
+ // we can use a library like "resize-observer-polyfill"
11
+ globalThis.ResizeObserver ??= class {
12
+ observe(_target: Element) {
13
+ /* noop */
14
+ }
15
+ unobserve(_target: Element) {
16
+ /* noop */
17
+ }
18
+ disconnect() {
19
+ /* noop */
20
+ }
21
+ } as never;
22
+
8
23
  // Global setup for all tests
9
24
  beforeEach(() => {
10
25
  // Reset all mocks before each test
@@ -9,6 +9,7 @@ import {
9
9
  SearchIcon,
10
10
  } from "lucide-react";
11
11
  import React from "react";
12
+ import { useLocale } from "react-aria";
12
13
  import type { GetRowIds } from "@/plugins/impl/DataTablePlugin";
13
14
  import { cn } from "@/utils/cn";
14
15
  import type { PanelType } from "../editor/chrome/panels/context-aware-panel/context-aware-panel";
@@ -16,7 +17,7 @@ import { Button } from "../ui/button";
16
17
  import { Tooltip } from "../ui/tooltip";
17
18
  import { toast } from "../ui/use-toast";
18
19
  import { type DownloadActionProps, DownloadAs } from "./download-actions";
19
- import { DataTablePagination } from "./pagination";
20
+ import { DataTablePagination, prettifyRowColumnCount } from "./pagination";
20
21
  import type { DataTableSelection } from "./types";
21
22
 
22
23
  interface TableActionsProps<TData> {
@@ -64,6 +65,7 @@ export const TableActions = <TData,>({
64
65
  isPanelOpen,
65
66
  tableLoading,
66
67
  }: TableActionsProps<TData>) => {
68
+ const { locale } = useLocale();
67
69
  const handleSelectAllRows = (value: boolean) => {
68
70
  if (!onRowSelectionChange) {
69
71
  return;
@@ -173,7 +175,7 @@ export const TableActions = <TData,>({
173
175
  </>
174
176
  )}
175
177
 
176
- {pagination && (
178
+ {pagination ? (
177
179
  <DataTablePagination
178
180
  totalColumns={totalColumns}
179
181
  selection={selection}
@@ -182,6 +184,10 @@ export const TableActions = <TData,>({
182
184
  tableLoading={tableLoading}
183
185
  showPageSizeSelector={showPageSizeSelector}
184
186
  />
187
+ ) : (
188
+ <span className="text-xs text-muted-foreground px-2">
189
+ {prettifyRowColumnCount(table.getRowCount(), totalColumns, locale)}
190
+ </span>
185
191
  )}
186
192
  <div className="ml-auto">
187
193
  {downloadAs && (
@@ -101,6 +101,69 @@ describe("DataTable", () => {
101
101
  expect(rows[2]).toHaveAttribute("title", "Jim Halpert");
102
102
  });
103
103
 
104
+ it("does not virtualize small datasets without pagination", () => {
105
+ const testData = Array.from({ length: 50 }, (_, i) => ({
106
+ id: i,
107
+ name: `Item ${i}`,
108
+ }));
109
+
110
+ const columns: ColumnDef<TestData>[] = [
111
+ { accessorKey: "id", header: "ID" },
112
+ { accessorKey: "name", header: "Name" },
113
+ ];
114
+
115
+ render(
116
+ <TooltipProvider>
117
+ <DataTable
118
+ data={testData}
119
+ columns={columns}
120
+ selection={null}
121
+ totalRows={50}
122
+ totalColumns={2}
123
+ pagination={false}
124
+ />
125
+ </TooltipProvider>,
126
+ );
127
+
128
+ // All 50 data rows + 1 header row should be in the DOM (no virtualization)
129
+ const rows = screen.getAllByRole("row");
130
+ expect(rows).toHaveLength(51);
131
+ });
132
+
133
+ it("virtualizes large datasets — renders fewer rows than the full dataset", () => {
134
+ const testData = Array.from({ length: 200 }, (_, i) => ({
135
+ id: i,
136
+ name: `Item ${i}`,
137
+ }));
138
+
139
+ const columns: ColumnDef<TestData>[] = [
140
+ { accessorKey: "id", header: "ID" },
141
+ { accessorKey: "name", header: "Name" },
142
+ ];
143
+
144
+ render(
145
+ <TooltipProvider>
146
+ <DataTable
147
+ data={testData}
148
+ columns={columns}
149
+ selection={null}
150
+ totalRows={200}
151
+ totalColumns={2}
152
+ pagination={false}
153
+ />
154
+ </TooltipProvider>,
155
+ );
156
+
157
+ // In jsdom the virtualizer sees a 0-height container and renders 0 data
158
+ // rows (no layout engine). The key assertion is that significantly fewer
159
+ // than 200 rows are in the DOM, which catches regressions where
160
+ // virtualization is accidentally disabled and all rows are rendered.
161
+ const rows = screen.getAllByRole("row");
162
+ // Subtract 1 for the header row
163
+ const dataRows = rows.length - 1;
164
+ expect(dataRows).toBeLessThan(200);
165
+ });
166
+
104
167
  it("should display updated data after rerender with manual sorting and pagination", () => {
105
168
  // Simulates the bug from issue #8023:
106
169
  // When a user sorts a table, rows that moved from page 2 to page 1
@@ -38,12 +38,17 @@ import type { DownloadActionProps } from "./download-actions";
38
38
  import { FilterPills } from "./filter-pills";
39
39
  import { FocusRowFeature } from "./focus-row/feature";
40
40
  import { useColumnPinning } from "./hooks/use-column-pinning";
41
+ import { useScrollContainerHeight } from "./hooks/use-scroll-container-height";
41
42
  import { CellSelectionStats } from "./range-focus/cell-selection-stats";
42
43
  import { CellSelectionProvider } from "./range-focus/provider";
43
44
  import { DataTableBody, renderTableHeader } from "./renderers";
44
45
  import { SearchBar } from "./SearchBar";
45
46
  import { TableActions } from "./TableActions";
46
- import type { DataTableSelection, TooManyRows } from "./types";
47
+ import {
48
+ type DataTableSelection,
49
+ MIN_ROWS_TO_VIRTUALIZE,
50
+ type TooManyRows,
51
+ } from "./types";
47
52
  import { getStableRowId } from "./utils";
48
53
 
49
54
  interface DataTableProps<TData> extends Partial<DownloadActionProps> {
@@ -268,36 +273,9 @@ const DataTableInternal = <TData,>({
268
273
  });
269
274
 
270
275
  const rowViewerPanelOpen = isPanelOpen?.("row-viewer") ?? false;
276
+ const virtualize = !pagination && data.length > MIN_ROWS_TO_VIRTUALIZE;
271
277
 
272
- const tableRef = React.useRef<HTMLTableElement | null>(null);
273
-
274
- // Why use a ref to set max-height on the wrapper?
275
- // - position: sticky only works when the sticky element's nearest scrollable
276
- // ancestor is its immediate container. If max-height/overflow are applied
277
- // on a grandparent, sticky table headers (th) will not stick.
278
- // - We keep the scroll wrapper colocated with the base Table component, but
279
- // derive the scroll boundary from maxHeight here to avoid coupling UI base
280
- // components to data-table specifics or expanding their API surface.
281
- // - Setting styles on the table's direct wrapper ensures the header sticks
282
- // reliably across browsers without changing upstream components.
283
- React.useEffect(() => {
284
- if (!tableRef.current) {
285
- return;
286
- }
287
- const wrapper = tableRef.current.parentElement as HTMLDivElement | null;
288
- if (!wrapper) {
289
- return;
290
- }
291
- if (maxHeight) {
292
- wrapper.style.maxHeight = `${maxHeight}px`;
293
- // Ensure wrapper scrolls
294
- if (!wrapper.style.overflow) {
295
- wrapper.style.overflow = "auto";
296
- }
297
- } else {
298
- wrapper.style.removeProperty("max-height");
299
- }
300
- }, [maxHeight]);
278
+ const tableRef = useScrollContainerHeight({ maxHeight, virtualize });
301
279
 
302
280
  return (
303
281
  <div className={cn(wrapperClassName, "flex flex-col space-y-1")}>
@@ -317,13 +295,14 @@ const DataTableInternal = <TData,>({
317
295
  {showLoadingBar && (
318
296
  <thead className="absolute top-0 left-0 h-[3px] w-1/2 bg-primary animate-slide" />
319
297
  )}
320
- {renderTableHeader(table, Boolean(maxHeight))}
298
+ {renderTableHeader(table, virtualize || Boolean(maxHeight))}
321
299
  <DataTableBody
322
300
  table={table}
323
301
  columns={columns}
324
302
  rowViewerPanelOpen={rowViewerPanelOpen}
325
303
  getRowIndex={getPaginatedRowIndex}
326
304
  viewedRowIdx={viewedRowIdx}
305
+ virtualize={virtualize}
327
306
  />
328
307
  </Table>
329
308
  </div>
@@ -0,0 +1,97 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { useEffect, useLayoutEffect, useRef } from "react";
4
+ import {
5
+ DEFAULT_VIRTUAL_ROWS,
6
+ TABLE_HEADER_HEIGHT_PX,
7
+ TABLE_ROW_HEIGHT_PX,
8
+ } from "../types";
9
+
10
+ /**
11
+ * Manages the scroll container's max-height for the data table.
12
+ *
13
+ * Why set max-height on the table's direct wrapper via a ref?
14
+ * - `position: sticky` only works when the sticky element's nearest scrollable
15
+ * ancestor is its immediate container. If max-height/overflow are applied on
16
+ * a grandparent, sticky `<th>` elements will not stick.
17
+ * - The <Table> UI component wraps <table> in a div with overflow-auto. We
18
+ * derive the scroll boundary from this wrapper (tableRef.parentElement) to
19
+ * keep sticky headers working without coupling base UI components to
20
+ * data-table specifics or expanding their API surface.
21
+ *
22
+ * 3 scenarios:
23
+ * - maxHeight applied directly. This always takes preference
24
+ * - Virtualize without maxHeight: observed via ResizeObserver on <thead>
25
+ * so the container reacts to header size changes (charts loading, toggles).
26
+ * - No maxHeight and no virtualization: render everything
27
+ * in practice virtualization kicks in after 100 rows with pagination disabled
28
+ */
29
+ export function useScrollContainerHeight({
30
+ maxHeight,
31
+ virtualize,
32
+ }: {
33
+ maxHeight?: number;
34
+ virtualize: boolean;
35
+ }) {
36
+ const tableRef = useRef<HTMLTableElement | null>(null);
37
+
38
+ // Handle explicit maxHeight and non-virtualize cases synchronously
39
+ // before paint to avoid flickering.
40
+ useLayoutEffect(() => {
41
+ if (!tableRef.current) {
42
+ return;
43
+ }
44
+ const wrapper = tableRef.current.parentElement as HTMLDivElement | null;
45
+ if (!wrapper) {
46
+ return;
47
+ }
48
+ if (maxHeight) {
49
+ wrapper.style.maxHeight = `${maxHeight}px`;
50
+ if (!wrapper.style.overflow) {
51
+ wrapper.style.overflow = "auto";
52
+ }
53
+ } else if (!virtualize) {
54
+ wrapper.style.removeProperty("max-height");
55
+ }
56
+ // When virtualizing without an explicit maxHeight, the ResizeObserver
57
+ // below handles setting maxHeight reactively based on actual header size.
58
+ }, [maxHeight, virtualize]);
59
+
60
+ // When virtualizing without an explicit maxHeight, observe the <thead> for
61
+ // size changes (column summaries, charts loading async, header toggle) and
62
+ // recompute the scroll container height accordingly.
63
+ useEffect(() => {
64
+ if (!virtualize || maxHeight) {
65
+ return;
66
+ }
67
+ const table = tableRef.current;
68
+ if (!table) {
69
+ return;
70
+ }
71
+ const wrapper = table.parentElement as HTMLDivElement | null;
72
+ const thead = table.querySelector("thead");
73
+ if (!wrapper || !thead) {
74
+ return;
75
+ }
76
+ const updateMaxHeight = () => {
77
+ const headerHeight =
78
+ thead.getBoundingClientRect().height || TABLE_HEADER_HEIGHT_PX;
79
+ // Skip virtual spacer rows — they have arbitrary heights for scroll offset.
80
+ const firstDataRow = table.querySelector(
81
+ "tbody tr:not([data-virtual-spacer])",
82
+ );
83
+ const rowHeight =
84
+ firstDataRow?.getBoundingClientRect().height || TABLE_ROW_HEIGHT_PX;
85
+ wrapper.style.maxHeight = `${DEFAULT_VIRTUAL_ROWS * rowHeight + headerHeight}px`;
86
+ };
87
+
88
+ // Set initial height
89
+ updateMaxHeight();
90
+
91
+ const observer = new ResizeObserver(updateMaxHeight);
92
+ observer.observe(thead);
93
+ return () => observer.disconnect();
94
+ }, [virtualize, maxHeight]);
95
+
96
+ return tableRef;
97
+ }