@marimo-team/islands 0.23.12-dev1 → 0.23.12-dev10

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 (90) hide show
  1. package/dist/{ConnectedDataExplorerComponent-WqG-xX4l.js → ConnectedDataExplorerComponent-Du3_nUzI.js} +13 -13
  2. package/dist/{ErrorBoundary-BNx_OSVo.js → ErrorBoundary-DE6tzZf-.js} +2 -2
  3. package/dist/{any-language-editor-rPSlOll9.js → any-language-editor-DN1R-1KZ.js} +5 -5
  4. package/dist/{button-vQhauTmO.js → button-BacYv-bE.js} +7 -1
  5. package/dist/{capabilities-BEHzIS99.js → capabilities-D_4LYhSU.js} +1 -1
  6. package/dist/{chat-ui-k2kqhCv5.js → chat-ui-BEOvjkmJ.js} +16 -16
  7. package/dist/{check-nrzHDi45.js → check-C9OoNtR4.js} +1 -1
  8. package/dist/{code-visibility-CqnbA4Ex.js → code-visibility-D1W_9sHW.js} +664 -663
  9. package/dist/{copy-UhDed7D4.js → copy-COam1EG7.js} +2 -2
  10. package/dist/{dist-DYGLrbYQ.js → dist--2Bqjvs0.js} +2 -2
  11. package/dist/{error-banner-BHAkVFc2.js → error-banner-DFPfz_Qf.js} +2 -2
  12. package/dist/{esm-Bqu9AE2K.js → esm-M837UxV5.js} +1 -1
  13. package/dist/{extends-9Yl5BEcg.js → extends-9MVIxxRo.js} +4 -4
  14. package/dist/{formats-BV4bOfMI.js → formats-d6MhLuQ9.js} +4 -4
  15. package/dist/{glide-data-editor-BDTq6YUb.js → glide-data-editor-DkzAInWG.js} +9 -9
  16. package/dist/{html-to-image-C86pQALH.js → html-to-image-Di0mtt6O.js} +73 -73
  17. package/dist/{input-AKkGXdyV.js → input-CbEz_aj_.js} +6 -6
  18. package/dist/{label-E3ZJXHu8.js → label-WfTSU8L4.js} +2 -2
  19. package/dist/{loader-YPuQvn1Y.js → loader-Boph2xIS.js} +1 -1
  20. package/dist/main.js +1619 -1592
  21. package/dist/{mermaid-QFAR9YgY.js → mermaid-CJW9vIyO.js} +5 -5
  22. package/dist/{process-output-nNw4OpSj.js → process-output-BLd4KuwX.js} +3 -3
  23. package/dist/{reveal-component-CKXMgI4_.js → reveal-component-BXP8Y-bq.js} +11 -11
  24. package/dist/{spec-B45_YCNI.js → spec-Bv-XlYiv.js} +4 -4
  25. package/dist/{strings-Cq2s9_EQ.js → strings-Dq_j3Rxw.js} +4 -4
  26. package/dist/style.css +2 -2
  27. package/dist/{swiper-component-BNa_4kh2.js → swiper-component-5HoSsPi1.js} +2 -2
  28. package/dist/{toDate-Do1xRzAo.js → toDate-D-l5s8nn.js} +3 -3
  29. package/dist/{tooltip-Bz3OAwrU.js → tooltip-Czds6Qr8.js} +3 -3
  30. package/dist/{types-D8gEGs4R.js → types-C2Ir191_.js} +1 -1
  31. package/dist/{useAsyncData-CL3o2p4i.js → useAsyncData-1Dhzjfwf.js} +1 -1
  32. package/dist/{useDateFormatter-BC6iSz9g.js → useDateFormatter-CMnRuVmN.js} +2 -2
  33. package/dist/{useDeepCompareMemoize-BPx2MuOK.js → useDeepCompareMemoize-CDWT3BDz.js} +1 -1
  34. package/dist/{useIframeCapabilities-C6Ta3EyP.js → useIframeCapabilities-DWIYvDh7.js} +1 -1
  35. package/dist/{useLifecycle-C3Ec71q0.js → useLifecycle-AHlswLw-.js} +3 -3
  36. package/dist/{useTheme-ZhT6uIu3.js → useTheme-BrYvK-_A.js} +2 -2
  37. package/dist/{vega-component-C3AWYGAL.js → vega-component-Pk6lyc_a.js} +10 -10
  38. package/dist/{zod-DXqkaI_w.js → zod-CijjQh4u.js} +1 -1
  39. package/package.json +3 -3
  40. package/src/components/ai/display-helpers.tsx +5 -5
  41. package/src/components/app-config/ai-config.tsx +5 -5
  42. package/src/components/app-config/mcp-config.tsx +3 -3
  43. package/src/components/chat/acp/agent-panel.tsx +3 -3
  44. package/src/components/chat/acp/blocks.tsx +36 -38
  45. package/src/components/chat/acp/common.tsx +12 -16
  46. package/src/components/chat/acp/scroll-to-bottom-button.tsx +1 -1
  47. package/src/components/chat/acp/session-tabs.tsx +2 -2
  48. package/src/components/chat/chat-history-popover.tsx +1 -1
  49. package/src/components/chat/chat-panel.tsx +47 -23
  50. package/src/components/data-table/TableBottomBar.tsx +4 -1
  51. package/src/components/data-table/columns.tsx +2 -2
  52. package/src/components/data-table/data-table.tsx +26 -17
  53. package/src/components/data-table/filter-pill-editor.tsx +1 -1
  54. package/src/components/dependency-graph/minimap-content.tsx +1 -1
  55. package/src/components/editor/RecoveryButton.tsx +1 -1
  56. package/src/components/editor/actions/pair-with-agent-modal.tsx +2 -2
  57. package/src/components/editor/actions/useNotebookActions.tsx +4 -4
  58. package/src/components/editor/ai/__tests__/completion-utils.test.ts +91 -1
  59. package/src/components/editor/ai/ai-completion-editor.tsx +1 -1
  60. package/src/components/editor/ai/completion-utils.ts +86 -1
  61. package/src/components/editor/cell/CreateCellButton.tsx +1 -1
  62. package/src/components/editor/chrome/panels/empty-state.tsx +1 -1
  63. package/src/components/editor/chrome/panels/outline/floating-outline.tsx +1 -1
  64. package/src/components/editor/chrome/wrapper/pending-ai-cells.tsx +1 -1
  65. package/src/components/editor/columns/cell-column.tsx +1 -1
  66. package/src/components/editor/columns/sortable-column.tsx +2 -2
  67. package/src/components/editor/output/MarimoErrorOutput.tsx +1 -1
  68. package/src/components/editor/output/TextOutput.tsx +2 -2
  69. package/src/components/home/components.tsx +4 -4
  70. package/src/components/icons/github.tsx +21 -0
  71. package/src/components/icons/youtube.tsx +21 -0
  72. package/src/components/slides/minimap.tsx +2 -2
  73. package/src/components/slides/reveal-component.tsx +1 -1
  74. package/src/components/storage/components.tsx +3 -7
  75. package/src/components/ui/alert.tsx +1 -1
  76. package/src/components/ui/command.tsx +2 -2
  77. package/src/components/ui/reorderable-list.tsx +1 -1
  78. package/src/components/ui/table.tsx +2 -5
  79. package/src/core/codemirror/language/languages/sql/renderers.tsx +60 -68
  80. package/src/core/hotkeys/hotkeys.ts +1 -0
  81. package/src/plugins/impl/DataTablePlugin.tsx +20 -1
  82. package/src/plugins/impl/FileBrowserPlugin.tsx +165 -74
  83. package/src/plugins/impl/MatrixPlugin.tsx +2 -2
  84. package/src/plugins/impl/TabsPlugin.tsx +1 -1
  85. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +141 -1
  86. package/src/plugins/impl/__tests__/FileBrowserPlugin.test.tsx +314 -0
  87. package/src/plugins/impl/matplotlib/matplotlib-renderer.ts +1 -1
  88. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +155 -98
  89. package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +154 -1
  90. package/src/plugins/impl/mpl-interactive/mpl-websocket-shim.ts +10 -0
@@ -21,6 +21,7 @@ import React, {
21
21
  useMemo,
22
22
  useState,
23
23
  } from "react";
24
+ import { useLocale } from "react-aria";
24
25
  import useEvent from "react-use-event-hook";
25
26
  import { z } from "zod";
26
27
  import type { CellSelectionState } from "@/components/data-table/cell-selection/types";
@@ -75,6 +76,7 @@ import { useEffectSkipFirstRender } from "@/hooks/useEffectSkipFirstRender";
75
76
  import { Arrays } from "@/utils/arrays";
76
77
  import { Functions } from "@/utils/functions";
77
78
  import { Logger } from "@/utils/Logger";
79
+ import { prettyNumber } from "@/utils/numbers";
78
80
  import {
79
81
  generateColumns,
80
82
  inferFieldTypes,
@@ -699,7 +701,9 @@ export const LoadingDataTableComponent = memo(
699
701
  >(async () => {
700
702
  // TODO: props.get_column_summaries is always true,
701
703
  // so we are unable to detect if the function is registered
702
- if (props.totalRows === 0 || !props.showColumnSummaries) {
704
+ // Column summaries come from a kernel RPC, absent in static exports.
705
+ const isStatic = isStaticNotebook();
706
+ if (props.totalRows === 0 || !props.showColumnSummaries || isStatic) {
703
707
  return {
704
708
  data: null,
705
709
  stats: {},
@@ -875,10 +879,13 @@ const DataTableComponent = ({
875
879
  sizeBytesIsLoading?: boolean;
876
880
  }): JSX.Element => {
877
881
  const id = useId();
882
+ const { locale } = useLocale();
878
883
  const [viewedRowIdx, setViewedRowIdx] = useState(0);
879
884
  const { isPanelOpen, isAnyPanelOpen, togglePanel, panelType, setPanelType } =
880
885
  usePanelOwnership(id, cellId);
881
886
 
887
+ const isStatic = isStaticNotebook();
888
+
882
889
  const chartSpecModel = useMemo(() => {
883
890
  if (!columnSummaries) {
884
891
  return ColumnChartSpecModel.EMPTY;
@@ -949,6 +956,11 @@ const DataTableComponent = ({
949
956
  showDataTypes = false;
950
957
  }
951
958
 
959
+ // Row/cell selection writes back to the kernel, absent in static exports.
960
+ if (isStatic) {
961
+ selection = null;
962
+ }
963
+
952
964
  const columns = useMemo(
953
965
  () =>
954
966
  generateColumns({
@@ -1109,6 +1121,13 @@ const DataTableComponent = ({
1109
1121
  Result clipped. Showing {shownColumns} of {totalColumns} columns.
1110
1122
  </Banner>
1111
1123
  )}
1124
+ {isStatic && typeof totalRows === "number" && data.length < totalRows && (
1125
+ <Banner className="mb-1 rounded">
1126
+ Showing the first <strong>{prettyNumber(data.length, locale)}</strong>{" "}
1127
+ of <strong>{prettyNumber(totalRows, locale)}</strong> rows. Increase
1128
+ the table's <code>page_size</code> to embed more in the static export.
1129
+ </Banner>
1130
+ )}
1112
1131
  {columnSummaries?.is_disabled && (
1113
1132
  // Note: Keep the text in sync with the constant defined in table_manager.py
1114
1133
  // This hard-code can be removed when Functions can pass structural
@@ -1,7 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { type LucideIcon, CornerLeftUp } from "lucide-react";
4
- import { type JSX, useEffect, useState } from "react";
4
+ import { type JSX, useEffect, useLayoutEffect, useRef, useState } from "react";
5
5
  import { z } from "zod";
6
6
  import {
7
7
  FILE_ICON as FILE_TYPE_ICONS,
@@ -129,6 +129,7 @@ interface FileBrowserProps extends Data, PluginFunctions {
129
129
  }
130
130
 
131
131
  interface CheckboxOrIconProps {
132
+ name: string;
132
133
  isSelected: boolean;
133
134
  canSelect: boolean;
134
135
  Icon: LucideIcon;
@@ -136,6 +137,7 @@ interface CheckboxOrIconProps {
136
137
  }
137
138
 
138
139
  function CheckboxOrIcon({
140
+ name,
139
141
  isSelected,
140
142
  canSelect,
141
143
  Icon,
@@ -146,17 +148,21 @@ function CheckboxOrIcon({
146
148
  <>
147
149
  <Checkbox
148
150
  checked={isSelected}
151
+ aria-label={`Select ${name}`}
152
+ tabIndex={-1}
149
153
  onClick={(e) => {
150
154
  onSelect();
151
155
  e.stopPropagation();
152
156
  }}
153
- className={cn({ "hidden group-hover:flex": !isSelected })}
157
+ className={cn({
158
+ "hidden group-hover:flex group-focus:flex": !isSelected,
159
+ })}
154
160
  />
155
161
  <Icon
156
162
  size={16}
157
163
  className={cn("mr-2", {
158
164
  hidden: isSelected,
159
- "group-hover:hidden": !isSelected,
165
+ "group-hover:hidden group-focus:hidden": !isSelected,
160
166
  })}
161
167
  />
162
168
  </>
@@ -165,6 +171,18 @@ function CheckboxOrIcon({
165
171
  return <Icon size={16} className="mr-2" />;
166
172
  }
167
173
 
174
+ interface RowModel {
175
+ key: string;
176
+ name: string;
177
+ Icon: LucideIcon;
178
+ isSelected: boolean;
179
+ canSelect: boolean;
180
+ /** Enter and mouse-click action. */
181
+ onPrimary: () => void;
182
+ /** Space action; null when the row has nothing to toggle. */
183
+ onToggleSelect: (() => void) | null;
184
+ }
185
+
168
186
  /**
169
187
  * File browser component.
170
188
  *
@@ -184,6 +202,13 @@ export const FileBrowser = ({
184
202
  const [path, setPath] = useInternalStateWithSync(initialPath);
185
203
  const [isUpdatingPath, setIsUpdatingPath] = useState(false);
186
204
  const [showLoadingOverlay, setShowLoadingOverlay] = useState(false);
205
+ const [activeIndex, setActiveIndex] = useState(0);
206
+ const rowRefs = useRef<(HTMLTableRowElement | null)[]>([]);
207
+ const gridRef = useRef<HTMLTableElement | null>(null);
208
+ // Set when navigation is triggered from within the grid, so focus can follow
209
+ // to the parent row after the new listing renders instead of falling to the
210
+ // body when the previously focused row unmounts.
211
+ const refocusParentRef = useRef(false);
187
212
 
188
213
  // HACK: use the random-id of the host element to force a re-render
189
214
  // when the random-id changes, this means the cell was re-rendered
@@ -210,6 +235,22 @@ export const FileBrowser = ({
210
235
  };
211
236
  }, [isPending]);
212
237
 
238
+ // Reset the roving tabindex whenever the listing reloads (a new path or a
239
+ // same-path refresh) so activeIndex never points past the current rows.
240
+ const listingKey = `${path}::${randomId}`;
241
+ const [prevListingKey, setPrevListingKey] = useState(listingKey);
242
+ if (prevListingKey !== listingKey) {
243
+ setPrevListingKey(listingKey);
244
+ setActiveIndex(0);
245
+ }
246
+
247
+ useLayoutEffect(() => {
248
+ if (refocusParentRef.current) {
249
+ refocusParentRef.current = false;
250
+ rowRefs.current[0]?.focus();
251
+ }
252
+ }, [listingKey]);
253
+
213
254
  const files = data?.files ?? [];
214
255
  const selectedPaths = new Set(value.map((x) => x.path));
215
256
  const canSelectDirectories =
@@ -269,6 +310,8 @@ export const FileBrowser = ({
269
310
  return;
270
311
  }
271
312
 
313
+ refocusParentRef.current =
314
+ gridRef.current?.contains(document.activeElement) ?? false;
272
315
  setPath(newPath);
273
316
  setIsUpdatingPath(false);
274
317
  }
@@ -333,78 +376,90 @@ export const FileBrowser = ({
333
376
  setValue([...value, ...filesInView]);
334
377
  }
335
378
 
336
- // Create rows for directories and files
337
- const fileRows: React.ReactNode[] = [];
338
-
339
- // Parent directory ".." row button
340
- fileRows.push(
341
- <TableRow
342
- className="hover:bg-accent select-none"
343
- key={"Parent directory"}
344
- onClick={() => setNewPath(PARENT_DIRECTORY)}
345
- >
346
- <TableCell className="w-[50px] pl-4">
347
- <CornerLeftUp size={16} />
348
- </TableCell>
349
- <TableCell>{PARENT_DIRECTORY}</TableCell>
350
- </TableRow>,
351
- );
379
+ const rowModels: RowModel[] = [
380
+ {
381
+ key: "parent",
382
+ name: PARENT_DIRECTORY,
383
+ Icon: CornerLeftUp,
384
+ isSelected: false,
385
+ canSelect: false,
386
+ onPrimary: () => setNewPath(PARENT_DIRECTORY),
387
+ onToggleSelect: null,
388
+ },
389
+ ...files.map((file): RowModel => {
390
+ let filePath = file.path;
391
+ if (filePath.startsWith("//")) {
392
+ filePath = filePath.slice(1) as FilePath;
393
+ }
352
394
 
353
- for (const file of files) {
354
- let filePath = file.path;
395
+ const canSelect =
396
+ (canSelectDirectories && file.is_directory) ||
397
+ (canSelectFiles && !file.is_directory);
398
+ const isSelected = selectedPaths.has(filePath);
399
+ const fileType: FileType = file.is_directory
400
+ ? "directory"
401
+ : guessFileType(file.name);
402
+
403
+ const toggle = () =>
404
+ handleSelection({
405
+ path: filePath,
406
+ name: file.name,
407
+ isDirectory: file.is_directory,
408
+ });
409
+
410
+ return {
411
+ key: file.id,
412
+ name: file.name,
413
+ Icon: FILE_TYPE_ICONS[fileType],
414
+ isSelected,
415
+ canSelect,
416
+ onPrimary: file.is_directory
417
+ ? () => setNewPath(filePath)
418
+ : canSelect
419
+ ? toggle
420
+ : () => {},
421
+ onToggleSelect: canSelect ? toggle : null,
422
+ };
423
+ }),
424
+ ];
355
425
 
356
- if (filePath.startsWith("//")) {
357
- filePath = filePath.slice(1) as FilePath;
358
- }
426
+ function focusRow(index: number) {
427
+ setActiveIndex(index);
428
+ rowRefs.current[index]?.focus();
429
+ }
359
430
 
360
- // Click handler
361
- const handleClick = file.is_directory
362
- ? ({ path }: { path: string }) => setNewPath(path)
363
- : handleSelection;
364
-
365
- // Icon
366
- const fileType: FileType = file.is_directory
367
- ? "directory"
368
- : guessFileType(file.name);
369
-
370
- const Icon = FILE_TYPE_ICONS[fileType];
371
-
372
- const isSelected = selectedPaths.has(filePath);
373
-
374
- fileRows.push(
375
- <TableRow
376
- key={file.id}
377
- className={cn("hover:bg-accent group select-none", {
378
- "bg-primary/25 hover:bg-primary/35": isSelected,
379
- })}
380
- onClick={() =>
381
- handleClick({
382
- path: filePath,
383
- name: file.name,
384
- isDirectory: file.is_directory,
385
- })
386
- }
387
- >
388
- <TableCell className="w-[50px] pl-4">
389
- <CheckboxOrIcon
390
- isSelected={isSelected}
391
- canSelect={
392
- (canSelectDirectories && file.is_directory) ||
393
- (canSelectFiles && !file.is_directory)
394
- }
395
- Icon={Icon}
396
- onSelect={() =>
397
- handleSelection({
398
- path: filePath,
399
- name: file.name,
400
- isDirectory: file.is_directory,
401
- })
402
- }
403
- />
404
- </TableCell>
405
- <TableCell>{file.name}</TableCell>
406
- </TableRow>,
407
- );
431
+ function handleRowKeyDown(
432
+ e: React.KeyboardEvent<HTMLTableRowElement>,
433
+ index: number,
434
+ ) {
435
+ const lastIndex = rowModels.length - 1;
436
+ switch (e.key) {
437
+ case "ArrowDown":
438
+ e.preventDefault();
439
+ focusRow(Math.min(index + 1, lastIndex));
440
+ break;
441
+ case "ArrowUp":
442
+ e.preventDefault();
443
+ focusRow(Math.max(index - 1, 0));
444
+ break;
445
+ case "Home":
446
+ e.preventDefault();
447
+ focusRow(0);
448
+ break;
449
+ case "End":
450
+ e.preventDefault();
451
+ focusRow(lastIndex);
452
+ break;
453
+ case "Enter":
454
+ e.preventDefault();
455
+ rowModels[index].onPrimary();
456
+ break;
457
+ // Space is select-only; preventDefault stops the list from scrolling.
458
+ case " ":
459
+ e.preventDefault();
460
+ rowModels[index].onToggleSelect?.();
461
+ break;
462
+ }
408
463
  }
409
464
 
410
465
  // Get list of parent directories.
@@ -490,8 +545,44 @@ export const FileBrowser = ({
490
545
  <span>Listing files...</span>
491
546
  </div>
492
547
  )}
493
- <Table className="cursor-pointer table-fixed">
494
- <TableBody>{fileRows}</TableBody>
548
+ <Table
549
+ ref={gridRef}
550
+ className="cursor-pointer table-fixed"
551
+ role="grid"
552
+ aria-label="File browser"
553
+ aria-multiselectable={multiple}
554
+ >
555
+ <TableBody>
556
+ {rowModels.map((row, index) => (
557
+ <TableRow
558
+ key={row.key}
559
+ role="row"
560
+ ref={(el) => {
561
+ rowRefs.current[index] = el;
562
+ }}
563
+ tabIndex={index === activeIndex ? 0 : -1}
564
+ onFocus={() => setActiveIndex(index)}
565
+ onKeyDown={(e) => handleRowKeyDown(e, index)}
566
+ className={cn(
567
+ "hover:bg-accent group select-none focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-inset",
568
+ { "bg-primary/25 hover:bg-primary/35": row.isSelected },
569
+ )}
570
+ aria-selected={row.canSelect ? row.isSelected : undefined}
571
+ onClick={row.onPrimary}
572
+ >
573
+ <TableCell role="gridcell" className="w-[50px] pl-4">
574
+ <CheckboxOrIcon
575
+ name={row.name}
576
+ isSelected={row.isSelected}
577
+ canSelect={row.canSelect}
578
+ Icon={row.Icon}
579
+ onSelect={() => row.onToggleSelect?.()}
580
+ />
581
+ </TableCell>
582
+ <TableCell role="gridcell">{row.name}</TableCell>
583
+ </TableRow>
584
+ ))}
585
+ </TableBody>
495
586
  </Table>
496
587
  </div>
497
588
  <div className="mt-4">
@@ -244,10 +244,10 @@ const MatrixComponent = ({
244
244
  <td
245
245
  key={j}
246
246
  className={cn(
247
- "relative text-center min-w-14 h-8 px-2 transition-colors touch-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
247
+ "relative text-center min-w-14 h-8 px-2 transition-colors touch-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-hidden",
248
248
  isDisabled
249
249
  ? "cursor-default text-muted-foreground"
250
- : "cursor-ew-resize text-[var(--link)] hover:bg-accent",
250
+ : "cursor-ew-resize text-link hover:bg-accent",
251
251
  isActive && "bg-accent",
252
252
  j === 0 && "bracket-l",
253
253
  j === numCols - 1 && "bracket-r",
@@ -90,7 +90,7 @@ const TabComponent = ({
90
90
  className={cn(
91
91
  "scrollbar-thin",
92
92
  isVertical
93
- ? "flex flex-col items-stretch justify-start h-auto max-h-none shrink-0 min-w-[10rem] overflow-y-auto"
93
+ ? "flex flex-col items-stretch justify-start h-auto max-h-none shrink-0 min-w-40 overflow-y-auto"
94
94
  : "max-w-full overflow-x-auto justify-start",
95
95
  )}
96
96
  >
@@ -6,7 +6,7 @@ const TooltipProvider = Tooltip.Provider;
6
6
 
7
7
  import { act, render, screen, waitFor } from "@testing-library/react";
8
8
  import { Provider } from "jotai";
9
- import { beforeAll, describe, expect, it, vi } from "vitest";
9
+ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
10
10
  import { SetupMocks } from "@/__mocks__/common";
11
11
  import type { DownloadAsArgs } from "@/components/data-table/schemas";
12
12
  import type { FieldTypesWithExternalType } from "@/components/data-table/types";
@@ -17,6 +17,14 @@ import {
17
17
  LoadingDataTableComponent,
18
18
  } from "../DataTablePlugin";
19
19
 
20
+ // Default to normal (non-static) mode; individual tests flip this on.
21
+ const mockIsStatic = vi.fn().mockReturnValue(false);
22
+ vi.mock("@/core/static/static-state", async (importOriginal) => {
23
+ const actual =
24
+ await importOriginal<typeof import("@/core/static/static-state")>();
25
+ return { ...actual, isStaticNotebook: () => mockIsStatic() };
26
+ });
27
+
20
28
  beforeAll(() => {
21
29
  SetupMocks.resizeObserver();
22
30
  });
@@ -157,3 +165,135 @@ describe("LoadingDataTableComponent", () => {
157
165
  });
158
166
  });
159
167
  });
168
+
169
+ describe("static notebook control suppression", () => {
170
+ const fieldTypes: FieldTypesWithExternalType = [
171
+ ["id", ["integer", "integer"]],
172
+ ["name", ["string", "string"]],
173
+ ];
174
+
175
+ // Only the first page is embedded in a static export; the total is larger.
176
+ const TOTAL_ROWS = 50;
177
+ const PAGE_SIZE = 10;
178
+ const firstPage = JSON.stringify(
179
+ Array.from({ length: PAGE_SIZE }, (_, i) => ({
180
+ id: i + 1,
181
+ name: `item-${i + 1}`,
182
+ })),
183
+ );
184
+
185
+ const Wrapper = ({ children }: { children: React.ReactNode }) => (
186
+ <Provider store={store}>
187
+ <TooltipProvider>{children}</TooltipProvider>
188
+ </Provider>
189
+ );
190
+
191
+ const makeProps = () => {
192
+ const host = document.createElement("div");
193
+ return {
194
+ label: null,
195
+ totalRows: TOTAL_ROWS,
196
+ pagination: true,
197
+ pageSize: PAGE_SIZE,
198
+ selection: "multi" as const,
199
+ showDownload: true,
200
+ showFilters: true,
201
+ showColumnSummaries: true as const,
202
+ showDataTypes: true,
203
+ showPageSizeSelector: true,
204
+ showColumnExplorer: false,
205
+ showRowExplorer: false,
206
+ showChartBuilder: false,
207
+ rowHeaders: [] as FieldTypesWithExternalType,
208
+ fieldTypes,
209
+ totalColumns: 2,
210
+ maxColumns: "all" as const,
211
+ hasStableRowId: true,
212
+ lazy: false,
213
+ host,
214
+ showSearch: true,
215
+ value: [] as (number | string | { rowId: string; columnName?: string })[],
216
+ setValue: vi.fn(),
217
+ data: firstPage,
218
+ search: vi.fn().mockResolvedValue({
219
+ data: firstPage,
220
+ total_rows: TOTAL_ROWS,
221
+ cell_styles: null,
222
+ cell_hover_texts: null,
223
+ }),
224
+ download_as: vi.fn() as DownloadAsArgs,
225
+ get_column_summaries: vi.fn().mockResolvedValue({
226
+ data: null,
227
+ stats: {},
228
+ bin_values: {},
229
+ value_counts: {},
230
+ show_charts: false,
231
+ }),
232
+ get_data_url: vi.fn() as GetDataUrl,
233
+ get_row_ids: vi.fn() as GetRowIds,
234
+ get_size_bytes: vi.fn().mockResolvedValue({ size_bytes: null }),
235
+ };
236
+ };
237
+
238
+ beforeEach(() => {
239
+ mockIsStatic.mockReturnValue(false);
240
+ });
241
+
242
+ it("renders kernel-dependent controls in normal mode", async () => {
243
+ const props = makeProps();
244
+ render(
245
+ <Wrapper>
246
+ <LoadingDataTableComponent {...props} />
247
+ </Wrapper>,
248
+ );
249
+
250
+ await waitFor(() => {
251
+ expect(screen.getAllByRole("row").length).toBeGreaterThan(1);
252
+ });
253
+
254
+ expect(screen.getByPlaceholderText("Search...")).toBeInTheDocument();
255
+ expect(screen.getByTestId("next-page-button")).toBeInTheDocument();
256
+ expect(screen.getByTestId("select-all-checkbox")).toBeInTheDocument();
257
+ expect(props.get_column_summaries).toHaveBeenCalled();
258
+ expect(screen.queryByText(/Showing the first/)).not.toBeInTheDocument();
259
+ });
260
+
261
+ it("suppresses kernel-dependent controls in static mode", async () => {
262
+ mockIsStatic.mockReturnValue(true);
263
+ const props = makeProps();
264
+ render(
265
+ <Wrapper>
266
+ <LoadingDataTableComponent {...props} />
267
+ </Wrapper>,
268
+ );
269
+
270
+ await waitFor(() => {
271
+ expect(screen.getAllByRole("row").length).toBeGreaterThan(1);
272
+ });
273
+
274
+ // Top bar (search), pagination, and the selection column are gone.
275
+ expect(screen.queryByPlaceholderText("Search...")).not.toBeInTheDocument();
276
+ expect(screen.queryByTestId("next-page-button")).not.toBeInTheDocument();
277
+ expect(screen.queryByTestId("select-all-checkbox")).not.toBeInTheDocument();
278
+ // Column summaries never hit the kernel.
279
+ expect(props.get_column_summaries).not.toHaveBeenCalled();
280
+ // The truncation banner explains the missing rows.
281
+ expect(screen.getByText(/Showing the first/)).toBeInTheDocument();
282
+ });
283
+
284
+ it("suppresses search even when the author opts in via showSearch", async () => {
285
+ mockIsStatic.mockReturnValue(true);
286
+ const props = makeProps();
287
+ render(
288
+ <Wrapper>
289
+ <LoadingDataTableComponent {...props} showSearch={true} />
290
+ </Wrapper>,
291
+ );
292
+
293
+ await waitFor(() => {
294
+ expect(screen.getAllByRole("row").length).toBeGreaterThan(1);
295
+ });
296
+
297
+ expect(screen.queryByPlaceholderText("Search...")).not.toBeInTheDocument();
298
+ });
299
+ });