@marimo-team/islands 0.23.12-dev2 → 0.23.12-dev20

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 (106) 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-CsPewo4h.js} +16 -16
  7. package/dist/{check-nrzHDi45.js → check-C9OoNtR4.js} +1 -1
  8. package/dist/{code-visibility-DZ_6U5hT.js → code-visibility-02AuLxDs.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-DXwLcQ6l.js} +95 -88
  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 +1753 -1626
  21. package/dist/{mermaid-QFAR9YgY.js → mermaid-CJW9vIyO.js} +5 -5
  22. package/dist/{process-output-nNw4OpSj.js → process-output-C6_e1pT_.js} +3 -3
  23. package/dist/{reveal-component-BxDb5eK0.js → reveal-component-CX0nM3qj.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/go-to-definition/__tests__/commands.test.ts +67 -0
  80. package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +47 -0
  81. package/src/core/codemirror/go-to-definition/commands.ts +47 -30
  82. package/src/core/codemirror/go-to-definition/utils.ts +0 -1
  83. package/src/core/codemirror/language/languages/sql/renderers.tsx +60 -68
  84. package/src/core/codemirror/reactive-references/__tests__/analyzer.test.ts +54 -0
  85. package/src/core/codemirror/reactive-references/analyzer.ts +44 -35
  86. package/src/core/hotkeys/hotkeys.ts +1 -0
  87. package/src/core/islands/__tests__/bridge.test.ts +25 -0
  88. package/src/core/islands/__tests__/parse.test.ts +585 -1
  89. package/src/core/islands/__tests__/test-utils.tsx +10 -1
  90. package/src/core/islands/bridge.ts +6 -1
  91. package/src/core/islands/constants.ts +2 -0
  92. package/src/core/islands/parse.ts +293 -13
  93. package/src/plugins/impl/DataTablePlugin.tsx +20 -1
  94. package/src/plugins/impl/FileBrowserPlugin.tsx +165 -74
  95. package/src/plugins/impl/MatrixPlugin.tsx +2 -2
  96. package/src/plugins/impl/TabsPlugin.tsx +1 -1
  97. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +141 -1
  98. package/src/plugins/impl/__tests__/FileBrowserPlugin.test.tsx +314 -0
  99. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +4 -1
  100. package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +34 -0
  101. package/src/plugins/impl/anywidget/__tests__/model.test.ts +19 -0
  102. package/src/plugins/impl/anywidget/model.ts +15 -0
  103. package/src/plugins/impl/matplotlib/matplotlib-renderer.ts +1 -1
  104. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +155 -98
  105. package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +154 -1
  106. package/src/plugins/impl/mpl-interactive/mpl-websocket-shim.ts +10 -0
@@ -3,6 +3,7 @@
3
3
  import {
4
4
  ISLAND_DATA_ATTRIBUTES,
5
5
  ISLAND_TAG_NAMES,
6
+ ISLANDS_JSON_SCRIPT_TYPE,
6
7
  } from "@/core/islands/constants";
7
8
  import { Logger } from "@/utils/Logger";
8
9
 
@@ -23,6 +24,10 @@ export interface MarimoIslandApp {
23
24
  * ID since we allow multiple apps on the same page.
24
25
  */
25
26
  id: string;
27
+ /**
28
+ * Whether cells came from a supported JSON payload instead of DOM parsing.
29
+ */
30
+ payloadBacked?: boolean;
26
31
  /**
27
32
  * Cells in the app.
28
33
  */
@@ -42,6 +47,30 @@ interface MarimoIslandCell {
42
47
  * Index of the cell.
43
48
  */
44
49
  idx: number;
50
+ /**
51
+ * Stable cell identifier, when provided by the island payload.
52
+ */
53
+ cellId?: string;
54
+ /**
55
+ * Whether the generated marimo cell should be present but not executed.
56
+ */
57
+ disabled?: boolean;
58
+ }
59
+
60
+ interface MarimoIslandPayload {
61
+ schemaVersion: 1;
62
+ appId: string;
63
+ cells: MarimoIslandPayloadCell[];
64
+ }
65
+
66
+ interface MarimoIslandPayloadCell {
67
+ cellId: string;
68
+ code: string;
69
+ outputHtml: string;
70
+ outputMimetype: string;
71
+ reactive: boolean;
72
+ displayCode: boolean;
73
+ displayOutput: boolean;
45
74
  }
46
75
 
47
76
  /**
@@ -51,14 +80,20 @@ interface MarimoIslandCell {
51
80
  export function parseMarimoIslandApps(
52
81
  root: Document | Element = document,
53
82
  ): MarimoIslandApp[] {
54
- const embeds = root.querySelectorAll<HTMLElement>(ISLAND_TAG_NAMES.ISLAND);
83
+ const embeds = [
84
+ ...root.querySelectorAll<HTMLElement>(ISLAND_TAG_NAMES.ISLAND),
85
+ ];
86
+ const payloads = parseMarimoIslandPayloads(root);
55
87
  if (embeds.length === 0) {
56
88
  Logger.warn("No embedded marimo apps found.");
57
89
  return [];
58
90
  }
59
91
 
60
- // eslint-disable-next-line prefer-spread
61
- return parseIslandElementsIntoApps(Array.from(embeds));
92
+ if (payloads.length > 0) {
93
+ return parsePayloadBackedApps(embeds, payloads);
94
+ }
95
+
96
+ return parseIslandElementsIntoApps(embeds);
62
97
  }
63
98
 
64
99
  /**
@@ -90,11 +125,12 @@ export function parseIslandElementsIntoApps(
90
125
  continue;
91
126
  }
92
127
 
93
- if (!apps.has(appId)) {
94
- apps.set(appId, { id: appId, cells: [] });
128
+ let app = apps.get(appId);
129
+ if (!app) {
130
+ app = { id: appId, cells: [] };
131
+ apps.set(appId, app);
95
132
  }
96
133
 
97
- const app = apps.get(appId)!;
98
134
  const idx = app.cells.length;
99
135
  app.cells.push({
100
136
  output: cellData.output,
@@ -109,6 +145,159 @@ export function parseIslandElementsIntoApps(
109
145
  return [...apps.values()];
110
146
  }
111
147
 
148
+ function parsePayloadBackedApps(
149
+ embeds: HTMLElement[],
150
+ payloads: MarimoIslandPayload[],
151
+ ): MarimoIslandApp[] {
152
+ const apps = new Map<string, MarimoIslandApp>();
153
+ const matchedPayloadCells = new Map<MarimoIslandPayloadCell, HTMLElement>();
154
+ const consumedEmbeds = new Set<HTMLElement>();
155
+ const acceptedPayloads: MarimoIslandPayload[] = [];
156
+
157
+ for (const payload of payloads) {
158
+ let hasMatchedIsland = false;
159
+ for (const cell of payload.cells) {
160
+ const embed = findMatchingIsland({
161
+ embeds,
162
+ appId: payload.appId,
163
+ cell,
164
+ consumedEmbeds,
165
+ });
166
+ if (!embed) {
167
+ continue;
168
+ }
169
+ consumedEmbeds.add(embed);
170
+ matchedPayloadCells.set(cell, embed);
171
+ materializeIslandPayload(embed, cell);
172
+ hasMatchedIsland = true;
173
+ }
174
+ // Only payloads matched to island anchors can start runtime apps.
175
+ if (hasMatchedIsland) {
176
+ acceptedPayloads.push(payload);
177
+ }
178
+ }
179
+
180
+ const payloadAppIds = new Set(
181
+ acceptedPayloads.map((payload) => payload.appId),
182
+ );
183
+ const reactivePayloadAppIds = new Set(
184
+ acceptedPayloads
185
+ .filter((payload) => payload.cells.some((cell) => cell.reactive))
186
+ .map((payload) => payload.appId),
187
+ );
188
+
189
+ for (const payload of acceptedPayloads) {
190
+ for (const cell of payload.cells) {
191
+ const embed = matchedPayloadCells.get(cell);
192
+ // Static-only payload apps render from HTML and do not need a Pyodide
193
+ // session.
194
+ if (!reactivePayloadAppIds.has(payload.appId)) {
195
+ continue;
196
+ }
197
+
198
+ let app = apps.get(payload.appId);
199
+ if (!app) {
200
+ app = { id: payload.appId, payloadBacked: true, cells: [] };
201
+ apps.set(payload.appId, app);
202
+ }
203
+
204
+ const idx = app.cells.length;
205
+ const appCell: MarimoIslandCell = {
206
+ cellId: cell.cellId,
207
+ output: cell.outputHtml,
208
+ code: cell.reactive ? cell.code : "",
209
+ idx: idx,
210
+ };
211
+ // Keep static cells in the generated file so later reactive cells keep
212
+ // stable runtime indices without executing static code.
213
+ if (!cell.reactive) {
214
+ appCell.disabled = true;
215
+ }
216
+ app.cells.push(appCell);
217
+ if (cell.reactive) {
218
+ embed?.setAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX, idx.toString());
219
+ }
220
+ }
221
+ }
222
+
223
+ // A supported payload is the runtime source for its app. Extra same-app DOM
224
+ // islands are disconnected from runtime binding.
225
+ for (const embed of embeds) {
226
+ const appId = embed.getAttribute(ISLAND_DATA_ATTRIBUTES.APP_ID);
227
+ if (appId && payloadAppIds.has(appId) && !consumedEmbeds.has(embed)) {
228
+ embed.removeAttribute(ISLAND_DATA_ATTRIBUTES.CELL_ID);
229
+ embed.removeAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX);
230
+ embed.setAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE, "false");
231
+ }
232
+ }
233
+
234
+ const domOnlyEmbeds = embeds.filter((embed) => {
235
+ const appId = embed.getAttribute(ISLAND_DATA_ATTRIBUTES.APP_ID);
236
+ return !appId || !payloadAppIds.has(appId);
237
+ });
238
+
239
+ return [...apps.values(), ...parseIslandElementsIntoApps(domOnlyEmbeds)];
240
+ }
241
+
242
+ function findMatchingIsland({
243
+ embeds,
244
+ appId,
245
+ cell,
246
+ consumedEmbeds,
247
+ }: {
248
+ embeds: HTMLElement[];
249
+ appId: string;
250
+ cell: MarimoIslandPayloadCell;
251
+ consumedEmbeds: Set<HTMLElement>;
252
+ }): HTMLElement | undefined {
253
+ return embeds.find((embed) => {
254
+ if (consumedEmbeds.has(embed)) {
255
+ return false;
256
+ }
257
+ return (
258
+ embed.getAttribute(ISLAND_DATA_ATTRIBUTES.APP_ID) === appId &&
259
+ embed.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_ID) === cell.cellId
260
+ );
261
+ });
262
+ }
263
+
264
+ function materializeIslandPayload(
265
+ embed: HTMLElement,
266
+ cell: MarimoIslandPayloadCell,
267
+ ): void {
268
+ embed.setAttribute(
269
+ ISLAND_DATA_ATTRIBUTES.REACTIVE,
270
+ JSON.stringify(cell.reactive),
271
+ );
272
+ // The runtime file is synthesized from payload order, so DOM anchors bind
273
+ // by index.
274
+ embed.removeAttribute(ISLAND_DATA_ATTRIBUTES.CELL_ID);
275
+ if (!cell.reactive) {
276
+ embed.removeAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX);
277
+ }
278
+
279
+ const output = ensureIslandChild(embed, ISLAND_TAG_NAMES.CELL_OUTPUT);
280
+ output.innerHTML = cell.displayOutput ? cell.outputHtml : "";
281
+
282
+ const code = ensureIslandChild(embed, ISLAND_TAG_NAMES.CELL_CODE);
283
+ code.hidden = true;
284
+ code.textContent = encodeURIComponent(cell.code);
285
+
286
+ const editor = embed.querySelector<HTMLElement>(ISLAND_TAG_NAMES.CODE_EDITOR);
287
+ if (editor) {
288
+ editor.setAttribute("data-initial-value", JSON.stringify(cell.code));
289
+ }
290
+ }
291
+
292
+ function ensureIslandChild(embed: HTMLElement, tagName: string): HTMLElement {
293
+ let child = embed.querySelector<HTMLElement>(tagName);
294
+ if (!child) {
295
+ child = embed.ownerDocument.createElement(tagName);
296
+ embed.appendChild(child);
297
+ }
298
+ return child;
299
+ }
300
+
112
301
  /**
113
302
  * Parses a single island element into cell data
114
303
  * @param embed - The island HTML element
@@ -132,26 +321,35 @@ export function parseIslandElement(
132
321
  };
133
322
  }
134
323
 
135
- export function createMarimoFile(app: { cells: { code: string }[] }): string {
324
+ export function createMarimoFile(app: {
325
+ cells: { code: string; disabled?: boolean }[];
326
+ }): string {
136
327
  const lines = [
137
328
  "import marimo",
138
329
  "app = marimo.App()",
139
330
  app.cells
140
331
  .map((cell) => {
141
- // Add 4 spaces to each line
142
- const code = cell.code
143
- .split("\n")
144
- .map((line) => ` ${line}`)
145
- .join("\n");
332
+ // Disabled payload cells are placeholders. Emit pass so static code
333
+ // does not define names in the runtime graph.
334
+ const sourceCode = cell.disabled ? "" : cell.code;
335
+ const code = sourceCode
336
+ ? sourceCode
337
+ .split("\n")
338
+ .map((line) => ` ${line}`)
339
+ .join("\n")
340
+ : " pass";
146
341
 
147
342
  // TODO: Handle async cells better
148
343
  // This is probably not the best way to check if the code is async
149
344
  // Ideally this is pushed into the Python code
150
345
  const isAsync = code.includes("await ");
151
346
  const prefix = isAsync ? "async def" : "def";
347
+ const decorator = cell.disabled
348
+ ? "@app.cell(disabled=True)"
349
+ : "@app.cell";
152
350
 
153
351
  // Wrap in a function
154
- return `@app.cell\n${prefix} __():\n${code}\n return`;
352
+ return `${decorator}\n${prefix} __():\n${code}\n return`;
155
353
  })
156
354
  .join("\n"),
157
355
  ];
@@ -204,3 +402,85 @@ export function extractIslandCodeFromEmbed(embed: HTMLElement): string {
204
402
 
205
403
  return "";
206
404
  }
405
+
406
+ function parseMarimoIslandPayloads(
407
+ root: Document | Element,
408
+ ): MarimoIslandPayload[] {
409
+ const scripts = root.querySelectorAll<HTMLScriptElement>(
410
+ `script[type="${ISLANDS_JSON_SCRIPT_TYPE}"]`,
411
+ );
412
+ const payloads: MarimoIslandPayload[] = [];
413
+
414
+ for (const script of scripts) {
415
+ if (isNestedIslandPayloadScript(script)) {
416
+ continue;
417
+ }
418
+ const payload = parseMarimoIslandPayload(script.textContent);
419
+ if (payload) {
420
+ payloads.push(payload);
421
+ }
422
+ }
423
+
424
+ return payloads;
425
+ }
426
+
427
+ function isNestedIslandPayloadScript(script: HTMLScriptElement): boolean {
428
+ return Boolean(
429
+ script.closest(ISLAND_TAG_NAMES.ISLAND) ||
430
+ script.closest(ISLAND_TAG_NAMES.CELL_OUTPUT),
431
+ );
432
+ }
433
+
434
+ function parseMarimoIslandPayload(
435
+ text: string | undefined | null,
436
+ ): MarimoIslandPayload | null {
437
+ if (!text) {
438
+ return null;
439
+ }
440
+
441
+ try {
442
+ const payload = JSON.parse(text);
443
+ if (isMarimoIslandPayload(payload)) {
444
+ return payload;
445
+ }
446
+ } catch {
447
+ return null;
448
+ }
449
+
450
+ return null;
451
+ }
452
+
453
+ function isMarimoIslandPayload(
454
+ payload: unknown,
455
+ ): payload is MarimoIslandPayload {
456
+ if (!isRecord(payload)) {
457
+ return false;
458
+ }
459
+ return (
460
+ payload.schemaVersion === 1 &&
461
+ typeof payload.appId === "string" &&
462
+ Array.isArray(payload.cells) &&
463
+ payload.cells.every(isMarimoIslandPayloadCell)
464
+ );
465
+ }
466
+
467
+ function isMarimoIslandPayloadCell(
468
+ cell: unknown,
469
+ ): cell is MarimoIslandPayloadCell {
470
+ if (!isRecord(cell)) {
471
+ return false;
472
+ }
473
+ return (
474
+ typeof cell.cellId === "string" &&
475
+ typeof cell.code === "string" &&
476
+ typeof cell.outputHtml === "string" &&
477
+ typeof cell.outputMimetype === "string" &&
478
+ typeof cell.reactive === "boolean" &&
479
+ typeof cell.displayCode === "boolean" &&
480
+ typeof cell.displayOutput === "boolean"
481
+ );
482
+ }
483
+
484
+ function isRecord(value: unknown): value is Record<string, unknown> {
485
+ return typeof value === "object" && value !== null;
486
+ }
@@ -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