@marimo-team/islands 0.22.5-dev9 → 0.22.5

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 (32) hide show
  1. package/dist/{ConnectedDataExplorerComponent-mLj6D01z.js → ConnectedDataExplorerComponent-D08JKcQg.js} +1 -1
  2. package/dist/{chat-ui-X5KPeHrU.js → chat-ui-BXYRQ5MH.js} +3 -3
  3. package/dist/main.js +212 -131
  4. package/dist/{mermaid-B93TKi2g.js → mermaid-BZ2YHhbi.js} +1 -1
  5. package/dist/{process-output-C0tmJosY.js → process-output-D_uZ0o1x.js} +2097 -2090
  6. package/dist/style.css +1 -1
  7. package/dist/{toDate-D1_ZulwM.js → toDate-D0QaHNwR.js} +8 -7
  8. package/dist/{useAsyncData-C9ez7Ilo.js → useAsyncData-BG3ULuDU.js} +1 -1
  9. package/dist/{useDeepCompareMemoize-BvvMxigY.js → useDeepCompareMemoize-CkSq3l3_.js} +1 -1
  10. package/dist/{vega-component-Bzzut3-P.js → vega-component-z4WGXPkf.js} +3 -3
  11. package/package.json +2 -2
  12. package/src/components/data-table/__tests__/columns.test.tsx +92 -13
  13. package/src/components/data-table/column-header.tsx +81 -56
  14. package/src/components/data-table/columns.tsx +25 -32
  15. package/src/components/data-table/data-table.tsx +8 -1
  16. package/src/components/data-table/renderers.tsx +19 -6
  17. package/src/components/data-table/types.ts +4 -0
  18. package/src/components/editor/Output.tsx +1 -1
  19. package/src/components/editor/__tests__/Output.test.tsx +36 -1
  20. package/src/core/cells/__tests__/cells.test.ts +41 -0
  21. package/src/core/cells/__tests__/collapseConsoleOutputs.test.ts +38 -0
  22. package/src/core/cells/cells.ts +1 -1
  23. package/src/core/cells/collapseConsoleOutputs.tsx +3 -0
  24. package/src/core/cells/document-changes.ts +12 -0
  25. package/src/core/runtime/__tests__/runtime.test.ts +138 -2
  26. package/src/core/runtime/runtime.ts +25 -5
  27. package/src/core/saving/file-state.ts +16 -0
  28. package/src/hooks/useAsyncData.ts +1 -1
  29. package/src/mount.tsx +17 -1
  30. package/src/plugins/impl/DataTablePlugin.tsx +1 -1
  31. package/src/plugins/impl/plotly/__tests__/selection.test.ts +22 -0
  32. package/src/plugins/impl/plotly/selection.ts +1 -0
@@ -498,7 +498,8 @@ var RuntimeManager = class {
498
498
  return g.pathname = `${g.pathname.replace(/\/$/, "")}/${e.replace(/^\//, "")}`, g.hash = "", g;
499
499
  }
500
500
  formatWsURL(e, m) {
501
- return asWsUrl(this.formatHttpURL(e, m, false).toString());
501
+ let h = this.formatHttpURL(e, m, false);
502
+ return !this.isSameOrigin && this.config.authToken && h.searchParams.set(KnownQueryParams.accessToken, this.config.authToken), asWsUrl(h.toString());
502
503
  }
503
504
  getWsURL(e) {
504
505
  let m = new URL(this.config.url), h = new URLSearchParams(m.search);
@@ -517,8 +518,8 @@ var RuntimeManager = class {
517
518
  }
518
519
  getLSPURL(e) {
519
520
  if (e === "copilot") {
520
- let m = this.formatWsURL(`/lsp/${e}`);
521
- return m.search = "", m;
521
+ let m = this.formatWsURL(`/lsp/${e}`), h = m.searchParams.get(KnownQueryParams.accessToken);
522
+ return m.search = "", h && m.searchParams.set(KnownQueryParams.accessToken, h), m;
522
523
  }
523
524
  return this.formatWsURL(`/lsp/${e}`);
524
525
  }
@@ -534,13 +535,13 @@ var RuntimeManager = class {
534
535
  let e = await fetch(this.healthURL().toString());
535
536
  if (e.redirected) {
536
537
  Logger.debug(`Runtime redirected to ${e.url}`);
537
- let m2 = e.url.replace(/\/health$/, "");
538
- this.config.url = m2;
538
+ let m2 = new URL(e.url);
539
+ m2.pathname = m2.pathname.replace(/\/health$/, ""), this.config.url = m2.toString();
539
540
  }
540
541
  let m = e.ok;
541
542
  return m && this.setDOMBaseUri(this.config.url), m;
542
- } catch {
543
- return false;
543
+ } catch (e) {
544
+ return Logger.error(`Failed to check health: ${e instanceof Error ? e.message : "Unknown error"}`, { cause: e }), false;
544
545
  }
545
546
  }
546
547
  setDOMBaseUri(e) {
@@ -80,7 +80,7 @@ function useAsyncData(e, s) {
80
80
  }, c[13] = f, c[14] = v, c[15] = y, c[16] = b) : b = c[16], b;
81
81
  }
82
82
  function _temp(e) {
83
- return e.status === "success" ? Result.loading(e.data) : Result.pending();
83
+ return e.status === "success" || e.status === "loading" ? Result.loading(e.data) : Result.pending();
84
84
  }
85
85
  export {
86
86
  useAsyncData as t
@@ -1,7 +1,7 @@
1
1
  import { s as __toESM } from "./chunk-BNovOVIE.js";
2
2
  import { t as require_react } from "./react-Bs6Z0kvn.js";
3
3
  import { t as require_compiler_runtime } from "./compiler-runtime-B_OLMU9S.js";
4
- import { t as toDate } from "./toDate-D1_ZulwM.js";
4
+ import { t as toDate } from "./toDate-D0QaHNwR.js";
5
5
  import { r as cva, y as cn } from "./button-DNlNlZY_.js";
6
6
  import { t as require_jsx_runtime } from "./jsx-runtime-9hcJiI23.js";
7
7
  import { C as dequal } from "./useTheme-CxjbgkRc.js";
@@ -1,7 +1,7 @@
1
1
  import { s as __toESM } from "./chunk-BNovOVIE.js";
2
2
  import { t as require_react } from "./react-Bs6Z0kvn.js";
3
3
  import { t as require_compiler_runtime } from "./compiler-runtime-B_OLMU9S.js";
4
- import { c as asRemoteURL, g as CircleQuestionMark } from "./toDate-D1_ZulwM.js";
4
+ import { c as asRemoteURL, g as CircleQuestionMark } from "./toDate-D0QaHNwR.js";
5
5
  import { c as Objects, g as Logger, h as Events, y as cn } from "./button-DNlNlZY_.js";
6
6
  import "./react-dom-BSUuJjCR.js";
7
7
  import { t as require_jsx_runtime } from "./jsx-runtime-9hcJiI23.js";
@@ -11,10 +11,10 @@ import { t as Tooltip } from "./tooltip-B5EnNyok.js";
11
11
  import { i as debounce_default } from "./constants-CvyfaCvs.js";
12
12
  import { n as useTheme, w as useEvent_default } from "./useTheme-CxjbgkRc.js";
13
13
  import { s as uniq } from "./arrays-beUWo8RF.js";
14
- import { a as AlertTitle, n as arrow, o as isValid, r as Alert, t as useDeepCompareMemoize } from "./useDeepCompareMemoize-BvvMxigY.js";
14
+ import { a as AlertTitle, n as arrow, o as isValid, r as Alert, t as useDeepCompareMemoize } from "./useDeepCompareMemoize-CkSq3l3_.js";
15
15
  import { n as formats } from "./vega-loader.browser-DqEcFOPD.js";
16
16
  import { a as getContainerWidth, n as vegaLoadData, s as tooltipHandler } from "./loader-Bd1kgLn7.js";
17
- import { t as useAsyncData } from "./useAsyncData-C9ez7Ilo.js";
17
+ import { t as useAsyncData } from "./useAsyncData-BG3ULuDU.js";
18
18
  import { t as j } from "./react-vega-CzRAIHrv.js";
19
19
  import "./defaultLocale-qS7DaAmi.js";
20
20
  import "./defaultLocale-Bxoo2-30.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.22.5-dev9",
3
+ "version": "0.22.5",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -120,7 +120,7 @@
120
120
  "marked": "^15.0.12",
121
121
  "mermaid": "^11.12.3",
122
122
  "partysocket": "1.1.10",
123
- "path-to-regexp": "^8.3.0",
123
+ "path-to-regexp": "^8.4.0",
124
124
  "plotly.js": "^3.3.1",
125
125
  "pyodide": "0.27.7",
126
126
  "react-arborist": "^3.4.3",
@@ -1,9 +1,9 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import type { Column } from "@tanstack/react-table";
4
- import { render } from "@testing-library/react";
4
+ import { fireEvent, render } from "@testing-library/react";
5
5
  import { I18nProvider } from "react-aria";
6
- import { describe, expect, it, test } from "vitest";
6
+ import { describe, expect, it, test, vi } from "vitest";
7
7
  import { TooltipProvider } from "@/components/ui/tooltip";
8
8
  import { parseContent } from "@/utils/url-parser";
9
9
  import {
@@ -312,7 +312,7 @@ describe("generateColumns", () => {
312
312
  expect(cell?.props.className).toContain("center");
313
313
  });
314
314
 
315
- it("should apply text justification to column header parents", () => {
315
+ it("should always left-align column headers regardless of text justification", () => {
316
316
  const columns = generateColumns({
317
317
  rowHeaders: [],
318
318
  selection: null,
@@ -330,33 +330,112 @@ describe("generateColumns", () => {
330
330
  columnDef: { meta: col.meta },
331
331
  });
332
332
 
333
- // Right-justified: parent wrapper should have items-end, sort/filter icons should flip to the left
333
+ // Even with right justification, header is left-aligned with sort + menu buttons
334
334
  const { container: rightContainer } = render(
335
335
  <TooltipProvider>
336
336
  {/* oxlint-disable-next-line typescript/no-explicit-any */}
337
337
  {(columns[0].header as any)({ column: mockColumn(columns[0]) })}
338
338
  </TooltipProvider>,
339
339
  );
340
+ expect(
341
+ rightContainer.querySelector("[data-testid='data-table-sort-button']"),
342
+ ).toBeTruthy();
343
+ expect(
344
+ rightContainer.querySelector(
345
+ "[data-testid='data-table-column-menu-button']",
346
+ ),
347
+ ).toBeTruthy();
348
+ // No flex-row-reverse or items-end on header
340
349
  const rightWrapper = rightContainer.firstElementChild;
341
- expect(rightWrapper?.className).toContain("items-end");
342
- const rightHeader = rightContainer.querySelector(
343
- "[data-testid='data-table-sort-button']",
344
- );
345
- expect(rightHeader?.className).toContain("flex-row-reverse");
350
+ expect(rightWrapper?.className).not.toContain("items-end");
351
+ expect(rightWrapper?.className).not.toContain("flex-row-reverse");
346
352
 
347
- // Center-justified: parent wrapper should have items-center, no flex-row-reverse
353
+ // Same for center-justified column
348
354
  const { container: centerContainer } = render(
349
355
  <TooltipProvider>
350
356
  {/* oxlint-disable-next-line typescript/no-explicit-any */}
351
357
  {(columns[1].header as any)({ column: mockColumn(columns[1]) })}
352
358
  </TooltipProvider>,
353
359
  );
360
+ expect(
361
+ centerContainer.querySelector("[data-testid='data-table-sort-button']"),
362
+ ).toBeTruthy();
363
+ expect(
364
+ centerContainer.querySelector(
365
+ "[data-testid='data-table-column-menu-button']",
366
+ ),
367
+ ).toBeTruthy();
354
368
  const centerWrapper = centerContainer.firstElementChild;
355
- expect(centerWrapper?.className).toContain("items-center");
356
- const centerHeader = centerContainer.querySelector(
369
+ expect(centerWrapper?.className).not.toContain("items-center");
370
+ expect(centerWrapper?.className).not.toContain("flex-row-reverse");
371
+ });
372
+
373
+ it("should cycle sort button through asc, desc, and clear on clicks", () => {
374
+ const columns = generateColumns({
375
+ rowHeaders: [],
376
+ selection: null,
377
+ fieldTypes,
378
+ });
379
+
380
+ const toggleSorting = vi.fn();
381
+ const clearSorting = vi.fn();
382
+ let sortDirection: false | "asc" | "desc" = false;
383
+
384
+ const mockColumn = (col: (typeof columns)[number]) => ({
385
+ id: col.id,
386
+ getCanSort: () => true,
387
+ getCanFilter: () => false,
388
+ getIsSorted: () => sortDirection,
389
+ getSortIndex: () => -1,
390
+ getFilterValue: () => undefined,
391
+ toggleSorting,
392
+ clearSorting,
393
+ columnDef: { meta: col.meta },
394
+ });
395
+
396
+ const mock = mockColumn(columns[0]);
397
+
398
+ const { container, rerender } = render(
399
+ <TooltipProvider>
400
+ {/* oxlint-disable-next-line typescript/no-explicit-any */}
401
+ {(columns[0].header as any)({ column: mock })}
402
+ </TooltipProvider>,
403
+ );
404
+
405
+ const sortButton = container.querySelector(
357
406
  "[data-testid='data-table-sort-button']",
358
407
  );
359
- expect(centerHeader?.className).not.toContain("flex-row-reverse");
408
+ expect(sortButton).toBeTruthy();
409
+
410
+ // first click unsorted > asc
411
+ fireEvent.click(sortButton!);
412
+ expect(toggleSorting).toHaveBeenCalledWith(false, true);
413
+
414
+ // Simulate asc state and re-render
415
+ sortDirection = "asc";
416
+ rerender(
417
+ <TooltipProvider>
418
+ {/* oxlint-disable-next-line typescript/no-explicit-any */}
419
+ {(columns[0].header as any)({ column: mock })}
420
+ </TooltipProvider>,
421
+ );
422
+
423
+ // second click asc >dsc
424
+ fireEvent.click(sortButton!);
425
+ expect(toggleSorting).toHaveBeenCalledWith(true, true);
426
+
427
+ // Simulate desc state and re-render
428
+ sortDirection = "desc";
429
+ rerender(
430
+ <TooltipProvider>
431
+ {/* oxlint-disable-next-line typescript/no-explicit-any */}
432
+ {(columns[0].header as any)({ column: mock })}
433
+ </TooltipProvider>,
434
+ );
435
+
436
+ // third click back to unsorted
437
+ fireEvent.click(sortButton!);
438
+ expect(clearSorting).toHaveBeenCalled();
360
439
  });
361
440
 
362
441
  it("should not include index column if it exists", () => {
@@ -2,7 +2,13 @@
2
2
  "use no memo";
3
3
 
4
4
  import type { Column, Table } from "@tanstack/react-table";
5
- import { FilterIcon, MinusIcon, TextIcon, XIcon } from "lucide-react";
5
+ import {
6
+ EllipsisIcon,
7
+ FilterIcon,
8
+ MinusIcon,
9
+ TextIcon,
10
+ XIcon,
11
+ } from "lucide-react";
6
12
  import { useMemo, useRef, useState } from "react";
7
13
  import { useLocale } from "react-aria";
8
14
  import {
@@ -69,7 +75,7 @@ interface DataTableColumnHeaderProps<
69
75
  > extends React.HTMLAttributes<HTMLDivElement> {
70
76
  column: Column<TData, TValue>;
71
77
  header: React.ReactNode;
72
- justify?: "left" | "center" | "right";
78
+ subheader?: React.ReactNode;
73
79
  calculateTopKRows?: CalculateTopKRows;
74
80
  table?: Table<TData>;
75
81
  }
@@ -77,7 +83,7 @@ interface DataTableColumnHeaderProps<
77
83
  export const DataTableColumnHeader = <TData, TValue>({
78
84
  column,
79
85
  header,
80
- justify,
86
+ subheader,
81
87
  className,
82
88
  calculateTopKRows,
83
89
  table,
@@ -92,49 +98,51 @@ export const DataTableColumnHeader = <TData, TValue>({
92
98
 
93
99
  // No sorting or filtering
94
100
  if (!column.getCanSort() && !column.getCanFilter()) {
95
- return <div className={cn(className)}>{header}</div>;
101
+ return (
102
+ <div className={cn(className)}>
103
+ {header}
104
+ {subheader}
105
+ </div>
106
+ );
96
107
  }
97
108
 
98
109
  const hasFilter = column.getFilterValue() !== undefined;
99
- const hideIcon = !column.getIsSorted() && !hasFilter;
100
110
 
101
111
  return (
102
112
  <>
103
- <DropdownMenu modal={false}>
104
- <DropdownMenuTrigger asChild={true}>
105
- <div
106
- className={cn(
107
- "group flex items-center my-1 space-between w-full select-none gap-2 border hover:border-border border-transparent hover:bg-(--slate-3) data-[state=open]:bg-(--slate-3) data-[state=open]:border-border rounded px-1 -mx-1",
108
- justify === "right" && "flex-row-reverse",
109
- className,
110
- )}
111
- data-testid="data-table-sort-button"
112
- >
113
- <span className="flex-1">{header}</span>
114
- <span
115
- className={cn(
116
- "h-5 py-1 px-1",
117
- hideIcon &&
118
- "invisible group-hover:visible data-[state=open]:visible",
119
- )}
120
- >
121
- {renderSortFilterIcon(column)}
122
- </span>
123
- </div>
124
- </DropdownMenuTrigger>
125
- <DropdownMenuContent align="start">
126
- {renderDataType(column)}
127
- {renderSorts(column, table)}
128
- {renderCopyColumn(column)}
129
- {renderColumnPinning(column)}
130
- {renderColumnWrapping(column)}
131
- {renderFormatOptions(column, locale)}
132
- <DropdownMenuSeparator />
133
- {renderMenuItemFilter(column)}
134
- {renderFilterByValues(column, setIsFilterValueOpen)}
135
- {hasFilter && <ClearFilterMenuItem column={column} />}
136
- </DropdownMenuContent>
137
- </DropdownMenu>
113
+ <div
114
+ className={cn("group flex flex-col my-1 w-full select-none", className)}
115
+ >
116
+ <div className="flex items-center gap-1">
117
+ <span>{header}</span>
118
+ {column.getCanSort() && <SortButton column={column} />}
119
+ <DropdownMenu modal={false}>
120
+ <DropdownMenuTrigger asChild={true}>
121
+ <button
122
+ type="button"
123
+ className="inline-flex items-center justify-center h-5 w-5 rounded hover:bg-(--slate-4) text-muted-foreground opacity-0 group-hover:opacity-100 focus:opacity-100 group-focus-within:opacity-100 data-[state=open]:opacity-100 data-[state=open]:text-accent-foreground"
124
+ aria-label="Column options"
125
+ data-testid="data-table-column-menu-button"
126
+ >
127
+ <EllipsisIcon className="h-3.5 w-3.5" />
128
+ </button>
129
+ </DropdownMenuTrigger>
130
+ <DropdownMenuContent align="start">
131
+ {renderDataType(column)}
132
+ {renderSorts(column, table)}
133
+ {renderCopyColumn(column)}
134
+ {renderColumnPinning(column)}
135
+ {renderColumnWrapping(column)}
136
+ {renderFormatOptions(column, locale)}
137
+ <DropdownMenuSeparator />
138
+ {renderMenuItemFilter(column)}
139
+ {renderFilterByValues(column, setIsFilterValueOpen)}
140
+ {hasFilter && <ClearFilterMenuItem column={column} />}
141
+ </DropdownMenuContent>
142
+ </DropdownMenu>
143
+ </div>
144
+ {subheader}
145
+ </div>
138
146
  {isFilterValueOpen && (
139
147
  <PopoverFilterByValues
140
148
  setIsFilterValueOpen={setIsFilterValueOpen}
@@ -146,28 +154,45 @@ export const DataTableColumnHeader = <TData, TValue>({
146
154
  );
147
155
  };
148
156
 
149
- export const DataTableColumnHeaderWithSummary = <TData, TValue>({
157
+ const SortButton = <TData, TValue>({
150
158
  column,
151
- header,
152
- summary,
153
- className,
154
- }: DataTableColumnHeaderProps<TData, TValue> & {
155
- summary: React.ReactNode;
159
+ }: {
160
+ column: Column<TData, TValue>;
156
161
  }) => {
162
+ const sortDirection = column.getIsSorted();
163
+
164
+ const handleClick = (e: React.MouseEvent) => {
165
+ e.stopPropagation();
166
+ if (!sortDirection) {
167
+ column.toggleSorting(false, true); // asc
168
+ } else if (sortDirection === "asc") {
169
+ column.toggleSorting(true, true); // desc
170
+ } else {
171
+ column.clearSorting();
172
+ }
173
+ };
174
+
157
175
  return (
158
- <div
176
+ <button
177
+ type="button"
178
+ onClick={handleClick}
159
179
  className={cn(
160
- "flex flex-col h-full pt-0.5 pb-3 justify-between items-start",
161
- className,
180
+ "inline-flex items-center justify-center h-5 w-5 rounded hover:bg-(--slate-4)",
181
+ sortDirection
182
+ ? "text-accent-foreground"
183
+ : "text-muted-foreground opacity-0 group-hover:opacity-100 focus:opacity-100 group-focus-within:opacity-100",
162
184
  )}
185
+ aria-label={
186
+ sortDirection === "asc"
187
+ ? "Sorted ascending, click to sort descending"
188
+ : sortDirection === "desc"
189
+ ? "Sorted descending, click to clear sort"
190
+ : "Sort column ascending"
191
+ }
192
+ data-testid="data-table-sort-button"
163
193
  >
164
- <DataTableColumnHeader
165
- column={column}
166
- header={header}
167
- className={className}
168
- />
169
- {summary}
170
- </div>
194
+ {renderSortFilterIcon(column)}
195
+ </button>
171
196
  );
172
197
  };
173
198
 
@@ -206,41 +206,30 @@ export function generateColumns<T>({
206
206
  </div>
207
207
  ) : null;
208
208
 
209
- const justify = getJustify(key);
210
-
211
- const headerWithType = (
212
- <div
209
+ const headerName = (
210
+ <span
213
211
  className={cn(
214
- "flex flex-col",
215
- justify === "center" && "items-center",
216
- justify === "right" && "items-end",
212
+ "font-bold",
213
+ headerTitle && "underline decoration-dotted",
217
214
  )}
218
215
  >
219
- <span
220
- className={cn(
221
- "font-bold",
222
- headerTitle && "underline decoration-dotted",
223
- )}
224
- >
225
- {key === "" ? " " : key}
226
- </span>
227
- {dtypeHeader}
228
- </div>
216
+ {key === "" ? " " : key}
217
+ </span>
229
218
  );
230
219
 
231
220
  const headerWithTooltip = headerTitle ? (
232
221
  <Tooltip content={headerTitle} delayDuration={300}>
233
- {headerWithType}
222
+ {headerName}
234
223
  </Tooltip>
235
224
  ) : (
236
- headerWithType
225
+ headerName
237
226
  );
238
227
 
239
228
  const dataTableColumnHeader = (
240
229
  <DataTableColumnHeader
241
230
  header={headerWithTooltip}
231
+ subheader={dtypeHeader}
242
232
  column={column}
243
- justify={justify}
244
233
  calculateTopKRows={calculateTopKRows}
245
234
  table={table}
246
235
  />
@@ -255,8 +244,6 @@ export function generateColumns<T>({
255
244
  <div
256
245
  className={cn(
257
246
  "flex flex-col h-full pt-0.5 pb-3 justify-between items-start",
258
- justify === "center" && "items-center",
259
- justify === "right" && "items-end",
260
247
  )}
261
248
  >
262
249
  {dataTableColumnHeader}
@@ -283,13 +270,13 @@ export function generateColumns<T>({
283
270
 
284
271
  const dataType = column.columnDef.meta?.dataType;
285
272
  const isNumeric = dataType === "number" || dataType === "integer";
286
- const cellStyles = getCellStyleClass(
273
+ const cellStyles = getCellStyleClass({
287
274
  justify,
288
275
  wrapped,
289
276
  canSelectCell,
290
- isCellSelected,
277
+ isSelected: isCellSelected,
291
278
  isNumeric,
292
- );
279
+ });
293
280
 
294
281
  const renderedCell = renderCellValue({
295
282
  column,
@@ -448,13 +435,19 @@ function getFilterTypeForFieldType(
448
435
  }
449
436
  }
450
437
 
451
- function getCellStyleClass(
452
- justify: "left" | "center" | "right" | undefined,
453
- wrapped: boolean | undefined,
454
- canSelectCell: boolean,
455
- isSelected: boolean,
456
- isNumeric?: boolean,
457
- ): string {
438
+ function getCellStyleClass({
439
+ justify = "left",
440
+ wrapped,
441
+ canSelectCell,
442
+ isSelected,
443
+ isNumeric = false,
444
+ }: {
445
+ justify: "left" | "center" | "right" | undefined;
446
+ wrapped: boolean | undefined;
447
+ canSelectCell: boolean;
448
+ isSelected: boolean;
449
+ isNumeric?: boolean;
450
+ }): string {
458
451
  return cn(
459
452
  canSelectCell && "cursor-pointer",
460
453
  isSelected &&
@@ -47,6 +47,7 @@ import { DataTableBody, renderTableHeader } from "./renderers";
47
47
  import { TableBottomBar } from "./TableBottomBar";
48
48
  import { TableTopBar } from "./TableTopBar";
49
49
  import {
50
+ AUTO_WIDTH_MAX_COLUMNS,
50
51
  type DataTableSelection,
51
52
  MIN_ROWS_TO_VIRTUALIZE,
52
53
  type TooManyRows,
@@ -300,7 +301,13 @@ const DataTableInternal = <TData,>({
300
301
  isAnyPanelOpen={isAnyPanelOpen}
301
302
  downloadAs={downloadAs}
302
303
  />
303
- <Table className="relative" ref={tableRef}>
304
+ <Table
305
+ className={cn(
306
+ "relative",
307
+ columns.length <= AUTO_WIDTH_MAX_COLUMNS ? "w-auto" : "w-full",
308
+ )}
309
+ ref={tableRef}
310
+ >
304
311
  {showLoadingBar && (
305
312
  <thead className="absolute top-0 left-0 h-[3px] w-1/2 bg-primary animate-slide" />
306
313
  )}
@@ -27,7 +27,7 @@ import { DataTableContextMenu } from "./context-menu";
27
27
  import { CellRangeSelectionIndicator } from "./range-focus/cell-selection-indicator";
28
28
  import { useCellRangeSelection } from "./range-focus/use-cell-range-selection";
29
29
  import { useScrollIntoViewOnFocus } from "./range-focus/use-scroll-into-view";
30
- import { TABLE_ROW_HEIGHT_PX } from "./types";
30
+ import { AUTO_WIDTH_MAX_COLUMNS, TABLE_ROW_HEIGHT_PX } from "./types";
31
31
  import { stringifyUnknownValue } from "./utils";
32
32
 
33
33
  export function renderTableHeader<TData>(
@@ -46,7 +46,7 @@ export function renderTableHeader<TData>(
46
46
  <TableHead
47
47
  key={header.id}
48
48
  className={cn(
49
- "h-auto min-h-10 whitespace-pre align-top",
49
+ "h-auto min-h-10 whitespace-pre align-top border-r border-r-border/75",
50
50
  className,
51
51
  )}
52
52
  style={style}
@@ -69,6 +69,13 @@ export function renderTableHeader<TData>(
69
69
  {renderHeaderGroup(table.getLeftHeaderGroups())}
70
70
  {renderHeaderGroup(table.getCenterHeaderGroups())}
71
71
  {renderHeaderGroup(table.getRightHeaderGroups())}
72
+ {table.getAllColumns().length <= AUTO_WIDTH_MAX_COLUMNS && (
73
+ <th
74
+ className="w-full border-0"
75
+ aria-hidden="true"
76
+ role="presentation"
77
+ />
78
+ )}
72
79
  </TableRow>
73
80
  </TableHeader>
74
81
  );
@@ -163,7 +170,7 @@ export const DataTableBody = <TData,>({
163
170
  {...getCellDomProps(cell.id)}
164
171
  key={cell.id}
165
172
  className={cn(
166
- "whitespace-pre truncate max-w-[300px] outline-hidden",
173
+ "whitespace-pre truncate max-w-[300px] outline-hidden border-r border-r-border/75",
167
174
  cell.column.getColumnWrapping &&
168
175
  cell.column.getColumnWrapping?.() === "wrap" &&
169
176
  COLUMN_WRAPPING_STYLES,
@@ -230,15 +237,21 @@ export const DataTableBody = <TData,>({
230
237
  {renderCells(row.getLeftVisibleCells())}
231
238
  {renderCells(row.getCenterVisibleCells())}
232
239
  {renderCells(row.getRightVisibleCells())}
240
+ {columns.length <= AUTO_WIDTH_MAX_COLUMNS && (
241
+ <td className="border-0" aria-hidden="true" role="presentation" />
242
+ )}
233
243
  </TableRow>
234
244
  );
235
245
  };
236
246
 
247
+ const hasFillerColumn = columns.length <= AUTO_WIDTH_MAX_COLUMNS;
248
+ const totalColSpan = columns.length + (hasFillerColumn ? 1 : 0);
249
+
237
250
  const renderRows = () => {
238
251
  if (rows.length === 0) {
239
252
  return (
240
253
  <TableRow>
241
- <TableCell colSpan={columns.length} className="h-24 text-center">
254
+ <TableCell colSpan={totalColSpan} className="h-24 text-center">
242
255
  No results.
243
256
  </TableCell>
244
257
  </TableRow>
@@ -255,7 +268,7 @@ export const DataTableBody = <TData,>({
255
268
  data-virtual-spacer=""
256
269
  style={{ height: virtualItems[0].start }}
257
270
  >
258
- <td colSpan={columns.length} />
271
+ <td colSpan={totalColSpan} />
259
272
  </tr>
260
273
  )}
261
274
  {virtualItems.map((vItem) => renderRow(rows[vItem.index]))}
@@ -266,7 +279,7 @@ export const DataTableBody = <TData,>({
266
279
  height: totalSize - (virtualItems.at(-1)?.end ?? totalSize),
267
280
  }}
268
281
  >
269
- <td colSpan={columns.length} />
282
+ <td colSpan={totalColSpan} />
270
283
  </tr>
271
284
  )}
272
285
  </>
@@ -16,6 +16,10 @@ declare module "@tanstack/react-table" {
16
16
  export const TABLE_ROW_HEIGHT_PX = 24;
17
17
  export const TABLE_HEADER_HEIGHT_PX = 40;
18
18
 
19
+ // Below this column count, the table uses w-auto with a filler column
20
+ // to prevent columns from stretching unnecessarily
21
+ export const AUTO_WIDTH_MAX_COLUMNS = 4;
22
+
19
23
  // Default number of visible rows when virtualizing without an explicit maxHeight.
20
24
  export const DEFAULT_VIRTUAL_ROWS = 15;
21
25
 
@@ -367,7 +367,7 @@ export const OutputArea = React.memo(
367
367
  forceExpand,
368
368
  className,
369
369
  }: OutputAreaProps) => {
370
- if (output === null) {
370
+ if (output == null) {
371
371
  return null;
372
372
  }
373
373
  if (output.channel === "output" && output.data === "") {