@marimo-team/islands 0.25.3-dev39 → 0.25.3-dev40

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 (118) hide show
  1. package/dist/{ConnectedDataExplorerComponent-7EaP9VWb.js → ConnectedDataExplorerComponent-COS3f8io.js} +14 -14
  2. package/dist/{ErrorBoundary-CWOZCRt2.js → ErrorBoundary-C7__jAZL.js} +8 -8
  3. package/dist/{abnfDiagram-VCTEODGH-z_OSynK8.js → abnfDiagram-VCTEODGH-DQ4bKxYa.js} +2 -2
  4. package/dist/any-language-editor-DQItQSua.js +75 -0
  5. package/dist/{architectureDiagram-5GKGNRK7-Cs8J8BZr.js → architectureDiagram-5GKGNRK7-BA7-SI7Q.js} +1 -1
  6. package/dist/{arrays-CN1djC4k.js → arrays-R6JsImiT.js} +8 -0
  7. package/dist/{button-CvjkwdSk.js → button-BxB3RDjN.js} +1 -1
  8. package/dist/{capabilities-CQ5IjhzL.js → capabilities-BgR0osfN.js} +2 -2
  9. package/dist/{chat-ui-BeKQyQQI.js → chat-ui-DGpBSln7.js} +15 -15
  10. package/dist/{check-BMKBBrdc.js → check-C209OH-W.js} +1 -1
  11. package/dist/{chunk-IMKFNOWR-aoSFasqF.js → chunk-IMKFNOWR-DPY3kqOV.js} +1 -1
  12. package/dist/{chunk-SHT3W25Y-CMaybO3O.js → chunk-SHT3W25Y-C14Ikodf.js} +1 -1
  13. package/dist/{chunk-SVP7TREG-BNBrTAw1.js → chunk-SVP7TREG-BKu0HO4I.js} +1 -1
  14. package/dist/{chunk-TICWLB2K-DXNxvUJn.js → chunk-TICWLB2K-iJDDWGtE.js} +1 -1
  15. package/dist/{classDiagram-ZZMXUADV-DXdCCDOg.js → classDiagram-ZZMXUADV-HuJXQI0F.js} +2 -2
  16. package/dist/{classDiagram-v2-VYDZK3BY-DyZqrtLP.js → classDiagram-v2-VYDZK3BY-CpmmpTbC.js} +2 -2
  17. package/dist/{code-visibility-Bt5UEhes.js → code-visibility-DO4MlViW.js} +6 -6
  18. package/dist/{common-qCKxRBN9.js → common-Ci7TC3e0.js} +89 -87
  19. package/dist/{copy-icon-BgUO8jsi.js → copy-icon-L-mFk59e.js} +3 -3
  20. package/dist/{cynefinDiagram-5FMLGOSQ-DWHuKJs8.js → cynefinDiagram-5FMLGOSQ-uh0puAW2.js} +1 -1
  21. package/dist/{dagre-GXQ25YYZ-DYLu-OCU.js → dagre-GXQ25YYZ-4LBeAAMz.js} +1 -1
  22. package/dist/{data-grid-overlay-editor-Do-9e8zr.js → data-grid-overlay-editor-DpH_AKdC.js} +1 -1
  23. package/dist/{diagram-S7CK7UJ4-PwwcUOHP.js → diagram-S7CK7UJ4-BXPmZ2sX.js} +1 -1
  24. package/dist/{diagram-UQ7AKVKN-CvJbL_IM.js → diagram-UQ7AKVKN--Xz9UNpC.js} +1 -1
  25. package/dist/{diagram-VX7I27RA-B5RkqnzN.js → diagram-VX7I27RA-rt8VIRNT.js} +1 -1
  26. package/dist/{diagram-Z3DM3KII-CyijwTR2.js → diagram-Z3DM3KII-Cs1GqfDD.js} +1 -1
  27. package/dist/{dialog-Cc479ZEH.js → dialog-CDbPtWUD.js} +3 -3
  28. package/dist/{dropdown-menu-BuE2b8Yb.js → dropdown-menu-ugNoZmKx.js} +76 -76
  29. package/dist/{ebnfDiagram-PWID7BFC-DG90ELFp.js → ebnfDiagram-PWID7BFC-CgTsgbmZ.js} +2 -2
  30. package/dist/{erDiagram-RLTQ6QDP-FMo_SpyX.js → erDiagram-RLTQ6QDP-CFoADGzT.js} +1 -1
  31. package/dist/{error-banner-CFPHFw4h.js → error-banner-DMIrHA-R.js} +2 -2
  32. package/dist/{error-entries-CnqKxhU9.js → error-entries-D576fzV9.js} +18 -18
  33. package/dist/{feedback-button-_v2ibEJ2.js → feedback-button-BV5AJ3nl.js} +9 -9
  34. package/dist/{flowDiagram-HODETNUW-DQ4JuadE.js → flowDiagram-HODETNUW-BM2lvKuH.js} +2 -2
  35. package/dist/{formats-DRHUWwSK.js → formats-DIVMVjeV.js} +3 -3
  36. package/dist/{glide-data-editor-D0KMp00B.js → glide-data-editor-CRK-O0KE.js} +326 -324
  37. package/dist/{glide-utils-pisbYVjH.js → glide-utils-Q2ZsuA3h.js} +15 -15
  38. package/dist/{globals-BCavQNmT.js → globals-H_ILLmr7.js} +3 -3
  39. package/dist/{html-to-image-BsG_zyNW.js → html-to-image-D3m5Ob5H.js} +11 -11
  40. package/dist/{infoDiagram-27XIBGKW-CRSDtRrd.js → infoDiagram-27XIBGKW-Crwjiywp.js} +1 -1
  41. package/dist/{ishikawaDiagram-5VMMS53U-DxLEUldB.js → ishikawaDiagram-5VMMS53U-D4US2ShV.js} +1 -1
  42. package/dist/{kanban-definition-UXKFOSKX-BLhvu8o4.js → kanban-definition-UXKFOSKX-Bk097CUX.js} +1 -1
  43. package/dist/{label-DoysmLMW.js → label-CuEjBqYP.js} +2 -2
  44. package/dist/{loader-BPdQhmlq.js → loader-Bm8cy3e8.js} +3 -3
  45. package/dist/main.js +2009 -1959
  46. package/dist/{mermaid-Bjm6C1qN.js → mermaid-6EkeBlhr.js} +36 -36
  47. package/dist/{mindmap-definition-YA3MSWOX-g2LwxnYQ.js → mindmap-definition-YA3MSWOX-DmORzfXQ.js} +1 -1
  48. package/dist/{number-overlay-editor-BHxF9uGj.js → number-overlay-editor-BQ2UqdED.js} +1 -1
  49. package/dist/{objects-CzUgoMIF.js → objects-DCmMvZtn.js} +1 -3
  50. package/dist/{once-BtE-wKRL.js → once-eG8Vmvjd.js} +1 -1
  51. package/dist/{pegDiagram-XKGWAZYB-CpMAt3Qs.js → pegDiagram-XKGWAZYB-DT5TL02S.js} +2 -2
  52. package/dist/{pieDiagram-E7YTZNPT-CVTthoZo.js → pieDiagram-E7YTZNPT-B1NwGcuH.js} +1 -1
  53. package/dist/{process-output-DgQrilBD.js → process-output-C6qc7hPS.js} +4 -4
  54. package/dist/{railroadDiagram-O6MQD6OU-ao8cqqFB.js → railroadDiagram-O6MQD6OU-BxoycdEu.js} +2 -2
  55. package/dist/{react-vega-BDpbvBCb.js → react-vega-C4T9Rb_w.js} +1 -1
  56. package/dist/{requirementDiagram-BXWQKSXE--J8vyfm-.js → requirementDiagram-BXWQKSXE-DHq_eRrm.js} +1 -1
  57. package/dist/{reveal-component-DNMX2D3g.js → reveal-component-Cs-0GwxY.js} +10 -10
  58. package/dist/{select-DemoKT4G.js → select-D2iqeJYk.js} +4 -4
  59. package/dist/{sequenceDiagram-WJ2MYXX4-CiQbn62x.js → sequenceDiagram-WJ2MYXX4-Ba11BAOu.js} +1 -1
  60. package/dist/{spec-D_u2ipzF.js → spec-BCCcvWPS.js} +4 -4
  61. package/dist/{stateDiagram-D77RDMKH-DVEeGs5b.js → stateDiagram-D77RDMKH-OHQbvkbP.js} +2 -2
  62. package/dist/{stateDiagram-v2-MP3YSRHH-1OQRe6iA.js → stateDiagram-v2-MP3YSRHH-BlIquvFJ.js} +2 -2
  63. package/dist/style.css +1 -1
  64. package/dist/{swimlanes-42K2YHIH-CqkGoGqn.js → swimlanes-42K2YHIH-DSlitS4s.js} +1 -1
  65. package/dist/{swimlanesDiagram-VR7AAH4N-CszjkrKo.js → swimlanesDiagram-VR7AAH4N-ClwuzFNO.js} +2 -2
  66. package/dist/{swiper-component-DKntyn8p.js → swiper-component-DfhRvAFi.js} +2 -2
  67. package/dist/{timeline-definition-24CTP7MA-DLRUNVAx.js → timeline-definition-24CTP7MA-hAzc5VkT.js} +1 -1
  68. package/dist/{toDate-gyi35XS2.js → toDate-CSY-j1Fi.js} +24 -24
  69. package/dist/{toast-PkdZraM8.js → toast-DxVfYlCp.js} +4 -4
  70. package/dist/{tooltip-CtBZ5q8p.js → tooltip-BCntm-jf.js} +2 -2
  71. package/dist/{use-restore-focus-CKjfFjLM.js → use-restore-focus-BhXhsNLV.js} +1 -1
  72. package/dist/{useAsyncData-BneN349z.js → useAsyncData-BbuzBAUp.js} +15 -14
  73. package/dist/{useDateFormatter-DkajGwwd.js → useDateFormatter-6AHbIR-J.js} +2 -2
  74. package/dist/{useDeepCompareMemoize-Z_7R8wTt.js → useDeepCompareMemoize-da5A3c-Y.js} +1 -1
  75. package/dist/{useIframeCapabilities-DBETvvPl.js → useIframeCapabilities-BkdeVdXC.js} +1 -1
  76. package/dist/{useLifecycle-Dfq8UD-b.js → useLifecycle-C3nT1kwI.js} +3 -3
  77. package/dist/{useTheme-Bu5bBgrh.js → useTheme-BC8Lof0B.js} +2 -2
  78. package/dist/{vega-component-1lFfDE_8.js → vega-component-Df45z17m.js} +147 -147
  79. package/dist/{vega-embed-container-BgKnsDbB.js → vega-embed-container--LYZqguO.js} +2 -2
  80. package/dist/{vega-loader.browser-CpbZAXub.js → vega-loader.browser-B9Pz0EGP.js} +1 -1
  81. package/dist/{vennDiagram-4TSXK5OY-CypgoBVZ.js → vennDiagram-4TSXK5OY-CINKaJgr.js} +1 -1
  82. package/dist/{wardleyDiagram-VM6X3IG4-B6xjiQ_g.js → wardleyDiagram-VM6X3IG4-KG2tlXg_.js} +1 -1
  83. package/dist/{xychartDiagram-S5SC5T6Z-e0PefDVs.js → xychartDiagram-S5SC5T6Z-XIH5cwgI.js} +1 -1
  84. package/package.json +1 -1
  85. package/src/components/app-config/ai-config.tsx +5 -1
  86. package/src/components/data-table/__tests__/date-popover.test.tsx +72 -0
  87. package/src/components/data-table/column-visibility-dropdown.tsx +8 -13
  88. package/src/components/data-table/columns.tsx +2 -1
  89. package/src/components/data-table/date-popover.tsx +6 -4
  90. package/src/components/data-table/row-viewer-panel/row-viewer.tsx +2 -2
  91. package/src/components/editor/ai/add-cell-with-ai.tsx +3 -3
  92. package/src/components/editor/connections/components.tsx +1 -1
  93. package/src/components/editor/output/MarimoTracebackOutput.tsx +1 -1
  94. package/src/components/pages/home-page.tsx +4 -4
  95. package/src/components/tracing/tracing.tsx +2 -2
  96. package/src/core/cells/pending-delete-service.ts +2 -6
  97. package/src/core/datasets/__tests__/data-source-discovery.test.ts +4 -4
  98. package/src/core/datasets/data-source-discovery.ts +4 -6
  99. package/src/core/hotkeys/actions.ts +2 -4
  100. package/src/core/runtime/runtime.ts +3 -4
  101. package/src/hooks/__tests__/useAsyncData.test.tsx +148 -0
  102. package/src/hooks/useAsyncData.ts +3 -3
  103. package/src/hooks/useDataSourceDiscovery.ts +6 -4
  104. package/src/hooks/useElapsedTime.ts +3 -3
  105. package/src/plugins/core/registerReactComponent.tsx +1 -1
  106. package/src/plugins/impl/DropdownPlugin.tsx +2 -6
  107. package/src/plugins/impl/FormPlugin.tsx +1 -1
  108. package/src/plugins/impl/code/any-language-editor.tsx +2 -2
  109. package/src/plugins/impl/data-editor/glide-data-editor.tsx +8 -5
  110. package/src/plugins/impl/data-editor/glide-utils.ts +3 -1
  111. package/src/plugins/impl/vega/make-selectable.ts +3 -6
  112. package/src/utils/__tests__/arrays.test.ts +35 -0
  113. package/src/utils/__tests__/numbers.test.ts +48 -1
  114. package/src/utils/__tests__/objects.test.ts +21 -0
  115. package/src/utils/arrays.ts +10 -0
  116. package/src/utils/numbers.ts +28 -17
  117. package/src/utils/objects.ts +3 -7
  118. package/dist/any-language-editor-D4q-IFcM.js +0 -76
@@ -1,4 +1,4 @@
1
- import { t as e } from "./objects-CzUgoMIF.js";
1
+ import { t as e } from "./objects-DCmMvZtn.js";
2
2
  import { a as t, n, r, t as i } from "./precisionRound-DhAzeRcm.js";
3
3
  import { i as a, n as o, r as s, t as c } from "./defaultLocale-DhPPeApV.js";
4
4
  import { C as l, E as u, P as d, S as f, T as p, _ as m, a as ee, c as te, d as h, i as ne, l as re, n as ie, o as ae, p as oe, r as se, s as ce, t as le, v as ue, w as de, x as g, y as _ } from "./defaultLocale-JI_lnoCe.js";
@@ -3,7 +3,7 @@ import { t } from "./src-CA_m4ONl.js";
3
3
  import { n } from "./chunk-Y2CYZVJY-DKKS1VyV.js";
4
4
  import { $ as r, H as i, K as a, U as o, a as s, b as c, c as l, et as u, f as d, nt as f, tt as p, v as m, w as h, y as g } from "./chunk-DU6HZSFF-BwiyaNpg.js";
5
5
  import { t as _ } from "./rough.esm-CtV9AnV1.js";
6
- import { p as v } from "./mermaid-Bjm6C1qN.js";
6
+ import { p as v } from "./mermaid-6EkeBlhr.js";
7
7
  //#region ../node_modules/.pnpm/khroma@2.1.0/node_modules/khroma/dist/methods/transparentize.js
8
8
  var y = (e, t) => p(e, "a", -t), b = 1e-10;
9
9
  function x(e, t) {
@@ -2,7 +2,7 @@ import { i as e } from "./chunk-75Z2AOVW-D7gxIs2Q.js";
2
2
  import { n as t } from "./src-CA_m4ONl.js";
3
3
  import { n } from "./chunk-Y2CYZVJY-DKKS1VyV.js";
4
4
  import { D as r, H as i, K as a, U as o, a as s, b as c, c as l, v as u, w as d, x as f, y as p } from "./chunk-DU6HZSFF-BwiyaNpg.js";
5
- import { p as m } from "./mermaid-Bjm6C1qN.js";
5
+ import { p as m } from "./mermaid-6EkeBlhr.js";
6
6
  import { n as h } from "./mermaid-parser.core-DxuSfYwJ.js";
7
7
  import { t as g } from "./chunk-JWPE2WC7-DJvv1t8U.js";
8
8
  //#region ../node_modules/.pnpm/mermaid@11.17.2/node_modules/mermaid/dist/chunks/mermaid.core/wardleyDiagram-VM6X3IG4.mjs
@@ -9,7 +9,7 @@ import { n as s } from "./chunk-Y2CYZVJY-DKKS1VyV.js";
9
9
  import { D as c, H as l, K as u, U as d, a as f, b as p, c as m, f as h, v as g, w as _, y as v, z as y } from "./chunk-DU6HZSFF-BwiyaNpg.js";
10
10
  import "./chunk-PWAF6VOD-CQw5qATZ.js";
11
11
  import { t as b } from "./chunk-GMAD6QVW-BhaJx0X3.js";
12
- import { p as x } from "./mermaid-Bjm6C1qN.js";
12
+ import { p as x } from "./mermaid-6EkeBlhr.js";
13
13
  //#region ../node_modules/.pnpm/d3-scale@4.0.2/node_modules/d3-scale/src/band.js
14
14
  function S() {
15
15
  var e = t().unknown(void 0), r = e.domain, i = e.range, o = 0, s = 1, c, l, u = !1, d = 0, f = 0, p = .5;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.25.3-dev39",
3
+ "version": "0.25.3-dev40",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -48,6 +48,7 @@ import { DEFAULT_AI_MODEL, type UserConfig } from "@/core/config/config-schema";
48
48
  import { isWasm } from "@/core/wasm/utils";
49
49
  import { cn } from "@/utils/cn";
50
50
  import { Events } from "@/utils/events";
51
+ import { Objects } from "@/utils/objects";
51
52
  import { Strings } from "@/utils/strings";
52
53
  import { AIModelDropdown, getProviderLabel } from "../ai/ai-model-dropdown";
53
54
  import {
@@ -681,7 +682,10 @@ export const CustomProvidersConfig: React.FC<AiConfigProps> = ({
681
682
  };
682
683
 
683
684
  const removeProvider = (providerName: string) => {
684
- const { [providerName]: _, ...rest } = customProviders;
685
+ const rest = Objects.filter(
686
+ customProviders,
687
+ (_, name) => name !== providerName,
688
+ );
685
689
  // Reset to clear nested dirty state, then set new value
686
690
  form.resetField("ai.custom_providers");
687
691
  form.setValue("ai.custom_providers", rest, { shouldDirty: true });
@@ -0,0 +1,72 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { render, screen } from "@testing-library/react";
3
+ import type { ReactNode } from "react";
4
+ import { I18nProvider } from "react-aria";
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import { DatePopover } from "../date-popover";
7
+
8
+ vi.mock("@/components/ui/tooltip", () => ({
9
+ Tooltip: ({ content }: { content: ReactNode }) => content,
10
+ }));
11
+
12
+ afterEach(() => {
13
+ vi.restoreAllMocks();
14
+ });
15
+
16
+ describe("DatePopover", () => {
17
+ it.each([0, 123])(
18
+ "updates the local timezone and time on rerender with %i milliseconds",
19
+ (milliseconds) => {
20
+ const NativeDateTimeFormat = Intl.DateTimeFormat;
21
+ let systemTimezone = "America/Los_Angeles";
22
+ vi.spyOn(Intl, "DateTimeFormat").mockImplementation(
23
+ function (locales, options) {
24
+ return new NativeDateTimeFormat(locales, {
25
+ ...options,
26
+ timeZone: options?.timeZone ?? systemTimezone,
27
+ });
28
+ },
29
+ );
30
+
31
+ const date = new Date("2026-01-15T12:00:00Z");
32
+ date.setUTCMilliseconds(milliseconds);
33
+ const content = () => (
34
+ <I18nProvider locale="en-US">
35
+ <DatePopover date={date} type="datetime">
36
+ timestamp
37
+ </DatePopover>
38
+ </I18nProvider>
39
+ );
40
+ const { rerender } = render(content());
41
+ const previousTime =
42
+ screen.getByText(systemTimezone).nextElementSibling?.textContent;
43
+ const utcTime = screen.getByText("UTC").nextElementSibling?.textContent;
44
+
45
+ systemTimezone = "Asia/Tokyo";
46
+ rerender(content());
47
+
48
+ const expected = new NativeDateTimeFormat("en-US", {
49
+ timeZone: systemTimezone,
50
+ ...(milliseconds
51
+ ? {
52
+ year: "numeric",
53
+ month: "2-digit",
54
+ day: "2-digit",
55
+ hour: "2-digit",
56
+ minute: "2-digit",
57
+ second: "2-digit",
58
+ fractionalSecondDigits: 3,
59
+ }
60
+ : { dateStyle: "long", timeStyle: "medium" }),
61
+ }).format(date);
62
+ expect(screen.queryByText("America/Los_Angeles")).toBeNull();
63
+ expect(
64
+ screen.getByText(systemTimezone).nextElementSibling?.textContent,
65
+ ).toBe(expected);
66
+ expect(expected).not.toBe(previousTime);
67
+ expect(screen.getByText("UTC").nextElementSibling?.textContent).toBe(
68
+ utcTime,
69
+ );
70
+ },
71
+ );
72
+ });
@@ -6,7 +6,7 @@
6
6
 
7
7
  import type { Table } from "@tanstack/react-table";
8
8
  import { Columns3Icon, EyeIcon, EyeOffIcon, ScanEyeIcon } from "lucide-react";
9
- import React, { useMemo } from "react";
9
+ import React from "react";
10
10
  import { ColumnName } from "@/components/datasources/components";
11
11
  import { Button } from "@/components/ui/button";
12
12
  import {
@@ -24,6 +24,7 @@ import {
24
24
  } from "@/components/ui/popover";
25
25
  import { type BulkAction, useSelectList } from "@/components/ui/select-core";
26
26
  import type { DataType } from "@/core/kernel/messages";
27
+ import { Arrays } from "@/utils/arrays";
27
28
  import { cn } from "@/utils/cn";
28
29
  import { Events } from "@/utils/events";
29
30
  import { smartMatchFilter } from "@/utils/smartMatch";
@@ -67,9 +68,8 @@ export const ColumnVisibilityDropdown = <TData,>({
67
68
  const hideableIds = userColumns
68
69
  .filter((column) => column.getCanHide())
69
70
  .map((column) => column.id);
70
- const visibleHideableCount = hideableIds.filter(
71
- (id) => !hiddenIds.includes(id),
72
- ).length;
71
+ const visibleHideableColumnIds = Arrays.difference(hideableIds, hiddenIds);
72
+ const visibleHideableCount = visibleHideableColumnIds.length;
73
73
 
74
74
  const applyHidden = (next: string[] | string | null) => {
75
75
  const hidden = new Set(Array.isArray(next) ? next : []);
@@ -106,15 +106,10 @@ export const ColumnVisibilityDropdown = <TData,>({
106
106
  .filter((option) => !option.disabled)
107
107
  .map((option) => option.value)
108
108
  : [];
109
- const showOnlyColumnState = useMemo(
110
- () => ({
111
- visibleHideableColumnIds: hideableIds.filter(
112
- (id) => !hiddenIds.includes(id),
113
- ),
114
- hideableColumnIdSet: new Set(hideableIds),
115
- }),
116
- [hideableIds, hiddenIds],
117
- );
109
+ const showOnlyColumnState = {
110
+ visibleHideableColumnIds,
111
+ hideableColumnIdSet: new Set(hideableIds),
112
+ };
118
113
  const hasSearchBulkActions =
119
114
  showOnlyMatchIds.length > 0 || matchingActions.length > 0;
120
115
 
@@ -135,6 +135,7 @@ export function generateColumns<T>({
135
135
  }): ColumnDef<T>[] {
136
136
  // Row-headers are typically index columns
137
137
  const rowHeadersSet = new Set(rowHeaders.map(([columnName]) => columnName));
138
+ const wrappedColumnsSet = new Set(wrappedColumns);
138
139
 
139
140
  const typesByColumn = Maps.keyBy(fieldTypes, (entry) => entry[0]);
140
141
 
@@ -287,7 +288,7 @@ export function generateColumns<T>({
287
288
  }
288
289
 
289
290
  const justify = getJustify(key);
290
- const wrapped = wrappedColumns?.includes(key);
291
+ const wrapped = wrappedColumnsSet.has(key);
291
292
  const isCellSelected = cell?.getIsSelected?.() || false;
292
293
  const canSelectCell =
293
294
  (selection === "single-cell" || selection === "multi-cell") &&
@@ -1,5 +1,5 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
- import React from "react";
2
+ import React, { useMemo } from "react";
3
3
  import { useDateFormatter, useLocale } from "react-aria";
4
4
  import { Tooltip } from "@/components/ui/tooltip";
5
5
 
@@ -45,6 +45,7 @@ export const DatePopover: React.FC<DatePopoverProps> = ({
45
45
 
46
46
  const TimezoneDisplay = ({ date }: { date: Date }) => {
47
47
  const { locale } = useLocale();
48
+ // The system timezone can change without the locale changing.
48
49
  const localTimezone = new Intl.DateTimeFormat(locale).resolvedOptions()
49
50
  .timeZone;
50
51
  const hasSubSeconds = date.getUTCMilliseconds() !== 0;
@@ -116,9 +117,10 @@ const RelativeTime = ({ date }: { date: Date }) => {
116
117
  const { locale } = useLocale();
117
118
 
118
119
  // Initialize relative time formatter with current locale and "auto" numeric style
119
- const relativeTimeFormatter = new Intl.RelativeTimeFormat(locale, {
120
- numeric: "auto",
121
- });
120
+ const relativeTimeFormatter = useMemo(
121
+ () => new Intl.RelativeTimeFormat(locale, { numeric: "auto" }),
122
+ [locale],
123
+ );
122
124
 
123
125
  const currentTime = new Date();
124
126
  // Get difference in seconds between now and the input date
@@ -34,6 +34,7 @@ import { DelayMount } from "@/components/utils/delay-mount";
34
34
  import { useAsyncData } from "@/hooks/useAsyncData";
35
35
  import { Banner, ErrorBanner } from "@/plugins/impl/common/error-banner";
36
36
  import type { GetRowResult } from "@/plugins/impl/DataTablePlugin";
37
+ import { Objects } from "@/utils/objects";
37
38
  import { NAMELESS_COLUMN_PREFIX, renderCellValue } from "../columns";
38
39
  import { prettifyRowCount } from "../pagination";
39
40
  import {
@@ -88,8 +89,7 @@ export const RowViewerPanel: React.FC<RowViewerPanelProps> = ({
88
89
  handleRowSelectionChange?.((prev) => {
89
90
  if (isRowSelected) {
90
91
  // Remove this row from selection
91
- const { [rowIdx]: removedRow, ...rest } = prev;
92
- return rest;
92
+ return Objects.filter(prev, (_, key) => key !== String(rowIdx));
93
93
  }
94
94
  // Add this row to selection
95
95
  return { ...prev, [rowIdx]: true };
@@ -106,9 +106,9 @@ export const AddCellWithAI: React.FC<{
106
106
  const runtimeManager = useRuntimeManager();
107
107
 
108
108
  const inputRef = useRef<ReactCodeMirrorRef>(null);
109
- const submissionController = useRef(
110
- new StagedCellSubmissionController(),
111
- ).current;
109
+ const [submissionController] = useState(
110
+ () => new StagedCellSubmissionController(),
111
+ );
112
112
 
113
113
  const fileInputRef = useRef<HTMLInputElement>(null);
114
114
  const { files, addFiles, removeFile } = useFileState();
@@ -48,7 +48,7 @@ export const SelectorButton: React.FC<{
48
48
  }> = ({ name, color, icon, onSelect }) => (
49
49
  <button
50
50
  type="button"
51
- className="py-3 flex flex-col items-center justify-center gap-1 transition-all hover:scale-105 hover:brightness-110 rounded shadow-sm-solid hover:shadow-md-solid"
51
+ className="py-3 flex flex-col items-center justify-center gap-1 transition-[scale,filter,box-shadow] hover:scale-105 hover:brightness-110 rounded shadow-sm-solid hover:shadow-md-solid"
52
52
  style={{ backgroundColor: color }}
53
53
  onClick={onSelect}
54
54
  >
@@ -248,7 +248,7 @@ export const replaceTracebackFilenames = (domNode: DOMNode) => {
248
248
  <Tooltip content={tooltipContent}>
249
249
  <button
250
250
  type="button"
251
- className="ml-1 p-1 rounded-sm hover:bg-muted transition-all inline"
251
+ className="ml-1 p-1 rounded-sm hover:bg-muted transition-colors inline"
252
252
  >
253
253
  <BugPlayIcon
254
254
  onClick={() => {
@@ -514,7 +514,7 @@ const MarimoFileComponent = ({ file }: { file: MarimoFile }) => {
514
514
 
515
515
  return (
516
516
  <a
517
- className="py-1.5 px-4 hover:bg-(--blue-2) hover:text-primary transition-all duration-300 cursor-pointer group relative flex gap-4 items-center"
517
+ className="py-1.5 px-4 hover:bg-(--blue-2) hover:text-primary transition-colors duration-300 cursor-pointer group relative flex gap-4 items-center"
518
518
  key={file.path}
519
519
  href={href.toString()}
520
520
  target={tabTarget(file.initializationId || file.path)}
@@ -542,7 +542,7 @@ const MarimoFileComponent = ({ file }: { file: MarimoFile }) => {
542
542
  </div>
543
543
  <ExternalLinkIcon
544
544
  size={20}
545
- className="group-hover:opacity-100 opacity-0 transition-all duration-300 text-primary"
545
+ className="group-hover:opacity-100 opacity-0 transition-opacity duration-300 text-primary"
546
546
  />
547
547
  </div>
548
548
  {!!file.lastModified && (
@@ -617,14 +617,14 @@ const CreateNewNotebook: React.FC = () => {
617
617
  <a
618
618
  className="relative rounded-lg p-6 group
619
619
  text-primary hover:bg-(--blue-2) shadow-md-solid shadow-accent border bg-(--blue-1)
620
- transition-all duration-300 cursor-pointer
620
+ transition-colors duration-300 cursor-pointer
621
621
  "
622
622
  href={url}
623
623
  target="_blank"
624
624
  rel="noreferrer"
625
625
  >
626
626
  <h2 className="text-lg font-semibold">Create a new notebook</h2>
627
- <div className="group-hover:opacity-100 opacity-0 absolute right-5 top-0 bottom-0 rounded-lg flex items-center justify-center transition-all duration-300">
627
+ <div className="group-hover:opacity-100 opacity-0 absolute right-5 top-0 bottom-0 rounded-lg flex items-center justify-center transition-opacity duration-300">
628
628
  <ExternalLinkIcon size={24} />
629
629
  </div>
630
630
  </a>
@@ -145,11 +145,11 @@ const TraceBlock: React.FC<{
145
145
  isMostRecentRun: boolean;
146
146
  chartPosition: ChartPosition;
147
147
  theme: ResolvedTheme;
148
- }> = ({ run, isMostRecentRun, chartPosition, isExpanded, theme }) => {
148
+ }> = ({ run, isMostRecentRun, chartPosition, isExpanded: expanded, theme }) => {
149
149
  const setExpandedRuns = useSetAtom(expandedRunsAtom);
150
150
  // We prefer the user's last click, but if they haven't clicked on this run,
151
151
  // we expand the most recent run by default, otherwise we collapse it.
152
- isExpanded = isExpanded ?? isMostRecentRun;
152
+ const isExpanded = expanded ?? isMostRecentRun;
153
153
 
154
154
  const onToggleExpanded = () => {
155
155
  setExpandedRuns((prev) => {
@@ -134,12 +134,8 @@ export function usePendingDeleteService() {
134
134
  submit({ cellIds, deleteCell, deleteManyCells, store });
135
135
  },
136
136
  clear,
137
- get idle() {
138
- return entries.size === 0;
139
- },
140
- get shouldConfirmDelete() {
141
- return entries.size > 1;
142
- },
137
+ idle: entries.size === 0,
138
+ shouldConfirmDelete: entries.size > 1,
143
139
  };
144
140
  }
145
141
 
@@ -185,8 +185,8 @@ describe("isDetectedSourceConnected", () => {
185
185
  });
186
186
  const emptySnapshot = {
187
187
  dialects: [] as string[],
188
- storageProtocols: [] as string[],
189
- storageBackendTypes: [] as string[],
188
+ storageProtocols: new Set<string>(),
189
+ storageBackendTypes: new Set<string>(),
190
190
  };
191
191
 
192
192
  it("does not hide a source when nothing is connected", () => {
@@ -242,7 +242,7 @@ describe("isDetectedSourceConnected", () => {
242
242
  expect(
243
243
  isDetectedSourceConnected(s3, {
244
244
  ...emptySnapshot,
245
- storageProtocols: ["s3"],
245
+ storageProtocols: new Set(["s3"]),
246
246
  }),
247
247
  ).toBe(true);
248
248
  });
@@ -262,7 +262,7 @@ describe("isDetectedSourceConnected", () => {
262
262
  expect(
263
263
  isDetectedSourceConnected(hf, {
264
264
  ...emptySnapshot,
265
- storageBackendTypes: ["huggingface"],
265
+ storageBackendTypes: new Set(["huggingface"]),
266
266
  }),
267
267
  ).toBe(true);
268
268
  });
@@ -27,8 +27,8 @@ export function matchesDiscoveryGroup(
27
27
  */
28
28
  export interface LiveConnectionSnapshot {
29
29
  dialects: readonly string[];
30
- storageProtocols: readonly string[];
31
- storageBackendTypes: readonly string[];
30
+ storageProtocols: ReadonlySet<string>;
31
+ storageBackendTypes: ReadonlySet<string>;
32
32
  }
33
33
 
34
34
  /**
@@ -47,15 +47,13 @@ export function isDetectedSourceConnected(
47
47
  }
48
48
 
49
49
  if (
50
- match.protocols.some((protocol) =>
51
- snapshot.storageProtocols.includes(protocol),
52
- )
50
+ match.protocols.some((protocol) => snapshot.storageProtocols.has(protocol))
53
51
  ) {
54
52
  return true;
55
53
  }
56
54
 
57
55
  return match.backendTypes.some((backendType) =>
58
- snapshot.storageBackendTypes.includes(backendType),
56
+ snapshot.storageBackendTypes.has(backendType),
59
57
  );
60
58
  }
61
59
 
@@ -2,6 +2,7 @@
2
2
  import { atom, useAtomValue, useSetAtom } from "jotai";
3
3
  import useEvent from "react-use-event-hook";
4
4
  import type { HotkeyAction } from "@/core/hotkeys/hotkeys";
5
+ import { Objects } from "@/utils/objects";
5
6
 
6
7
  /**
7
8
  * Map of registered keyboard shortcuts and their callbacks.
@@ -29,10 +30,7 @@ export function useSetRegisteredAction() {
29
30
  },
30
31
  ),
31
32
  unregisterAction: useEvent((shortcut: HotkeyAction) => {
32
- set((actions) => {
33
- const { [shortcut]: unused, ...rest } = actions;
34
- return rest;
35
- });
33
+ set((actions) => Objects.omit(actions, [shortcut]));
36
34
  }),
37
35
  };
38
36
  }
@@ -10,6 +10,8 @@ import { isStaticNotebook } from "../static/static-state";
10
10
  import { isWasm } from "../wasm/utils";
11
11
  import type { RuntimeConfig } from "./types";
12
12
 
13
+ const knownQueryParams = new Set(Object.values(KnownQueryParams));
14
+
13
15
  export class RuntimeManager {
14
16
  private initialHealthyCheck = new Deferred<void>();
15
17
  private config: RuntimeConfig;
@@ -72,10 +74,7 @@ export class RuntimeManager {
72
74
  }
73
75
 
74
76
  for (const [key, value] of currentParams.entries()) {
75
- if (
76
- restrictToKnownQueryParams &&
77
- !Object.values(KnownQueryParams).includes(key)
78
- ) {
77
+ if (restrictToKnownQueryParams && !knownQueryParams.has(key)) {
79
78
  continue;
80
79
  }
81
80
  baseUrl.searchParams.set(key, value);
@@ -0,0 +1,148 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { act, render, renderHook, waitFor } from "@testing-library/react";
3
+ import { Activity } from "react";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import { Deferred } from "@/utils/Deferred";
6
+ import { useAsyncData } from "../useAsyncData";
7
+
8
+ function AsyncDataStatus({ fetch }: { fetch: () => Promise<string> }) {
9
+ const result = useAsyncData(fetch, []);
10
+ return (
11
+ <output
12
+ data-status={result.status}
13
+ data-fetching={result.isFetching}
14
+ data-error={result.error?.message ?? ""}
15
+ >
16
+ {result.data}
17
+ </output>
18
+ );
19
+ }
20
+
21
+ describe("useAsyncData", () => {
22
+ it.each(["success", "error"] as const)(
23
+ "restarts fetching when Activity reopens after %s",
24
+ async (initialStatus) => {
25
+ const initial = new Deferred<string>();
26
+ const restarted = new Deferred<string>();
27
+ const fetch = vi
28
+ .fn(() => restarted.promise)
29
+ .mockImplementationOnce(() => initial.promise);
30
+ const view = render(
31
+ <Activity mode="visible">
32
+ <AsyncDataStatus fetch={fetch} />
33
+ </Activity>,
34
+ );
35
+ await act(async () => {
36
+ if (initialStatus === "success") {
37
+ initial.resolve("first");
38
+ } else {
39
+ initial.reject(new Error("offline"));
40
+ }
41
+ });
42
+ expect(view.getByRole("status").dataset.status).toBe(initialStatus);
43
+
44
+ view.rerender(
45
+ <Activity mode="hidden">
46
+ <AsyncDataStatus fetch={fetch} />
47
+ </Activity>,
48
+ );
49
+ view.rerender(
50
+ <Activity mode="visible">
51
+ <AsyncDataStatus fetch={fetch} />
52
+ </Activity>,
53
+ );
54
+
55
+ expect(fetch).toHaveBeenCalledTimes(2);
56
+ expect(view.getByRole("status").dataset).toMatchObject({
57
+ status: initialStatus === "success" ? "loading" : "pending",
58
+ error: "",
59
+ fetching: "true",
60
+ });
61
+ expect(view.getByRole("status").textContent).toBe(
62
+ initialStatus === "success" ? "first" : "",
63
+ );
64
+ await act(async () => restarted.resolve("latest"));
65
+ expect(view.getByRole("status").dataset).toMatchObject({
66
+ status: "success",
67
+ fetching: "false",
68
+ });
69
+ expect(view.getByRole("status").textContent).toBe("latest");
70
+ },
71
+ );
72
+
73
+ it("ignores a request canceled when Activity hides", async () => {
74
+ const canceled = new Deferred<string>();
75
+ const restarted = new Deferred<string>();
76
+ const fetch = vi
77
+ .fn(() => restarted.promise)
78
+ .mockImplementationOnce(() => canceled.promise);
79
+ const view = render(
80
+ <Activity mode="visible">
81
+ <AsyncDataStatus fetch={fetch} />
82
+ </Activity>,
83
+ );
84
+ view.rerender(
85
+ <Activity mode="hidden">
86
+ <AsyncDataStatus fetch={fetch} />
87
+ </Activity>,
88
+ );
89
+ view.rerender(
90
+ <Activity mode="visible">
91
+ <AsyncDataStatus fetch={fetch} />
92
+ </Activity>,
93
+ );
94
+ expect(fetch).toHaveBeenCalledTimes(2);
95
+ await act(async () => canceled.resolve("obsolete"));
96
+ expect(view.getByRole("status").dataset.status).toBe("pending");
97
+ expect(view.getByRole("status").textContent).toBe("");
98
+ await act(async () => restarted.resolve("latest"));
99
+ expect(view.getByRole("status").dataset.status).toBe("success");
100
+ expect(view.getByRole("status").textContent).toBe("latest");
101
+ });
102
+
103
+ it("preserves data while refetching and ignores obsolete requests", async () => {
104
+ const requests = [
105
+ new Deferred<string>(),
106
+ new Deferred<string>(),
107
+ new Deferred<string>(),
108
+ ];
109
+ const fetch = vi
110
+ .fn(() => requests[0].promise)
111
+ .mockImplementationOnce(() => requests[0].promise)
112
+ .mockImplementationOnce(() => requests[1].promise)
113
+ .mockImplementationOnce(() => requests[2].promise);
114
+ const { result, rerender } = renderHook(
115
+ ({ key }) => useAsyncData(fetch, [key]),
116
+ { initialProps: { key: 0 } },
117
+ );
118
+ expect(result.current.status).toBe("pending");
119
+ await act(async () => requests[0].resolve("first"));
120
+ expect(result.current.data).toBe("first");
121
+ rerender({ key: 1 });
122
+ expect(result.current).toMatchObject({ status: "loading", data: "first" });
123
+ rerender({ key: 2 });
124
+ await act(async () => requests[1].resolve("obsolete"));
125
+ expect(result.current).toMatchObject({ status: "loading", data: "first" });
126
+ await act(async () => requests[2].resolve("latest"));
127
+ expect(result.current).toMatchObject({ status: "success", data: "latest" });
128
+ });
129
+
130
+ it("retries after errors and allows manual updates during a request", async () => {
131
+ const request = new Deferred<string>();
132
+ const fetch = vi
133
+ .fn<() => Promise<string>>()
134
+ .mockRejectedValueOnce(new Error("offline"))
135
+ .mockImplementation(() => request.promise);
136
+ const { result } = renderHook(() => useAsyncData(fetch, []));
137
+ await waitFor(() => expect(result.current.status).toBe("error"));
138
+ act(() => result.current.refetch());
139
+ expect(result.current.status).toBe("pending");
140
+ act(() => result.current.setData("manual"));
141
+ expect(result.current).toMatchObject({ status: "success", data: "manual" });
142
+ await act(async () => request.resolve("response"));
143
+ expect(result.current).toMatchObject({
144
+ status: "success",
145
+ data: "response",
146
+ });
147
+ });
148
+ });
@@ -322,7 +322,7 @@ export function useAsyncData<T>(
322
322
  const [nonce, setNonce] = useState(0);
323
323
  const [result, setResult] = useState<
324
324
  PendingResult<T> | LoadingResult<T> | ErrorResult<T> | SuccessResult<T>
325
- >(Result.pending());
325
+ >(() => Result.pending());
326
326
 
327
327
  const asProps =
328
328
  typeof loaderOrProps === "function"
@@ -339,12 +339,12 @@ export function useAsyncData<T>(
339
339
  keepPrevious = true;
340
340
  },
341
341
  };
342
+ // Activity can restart this effect without changing dependencies, so
343
+ // loading state must follow the request lifecycle.
342
344
  setResult((prevResult) => {
343
- // If we have previous data, show reloading state
344
345
  if (prevResult.status === "success" || prevResult.status === "loading") {
345
346
  return Result.loading(prevResult.data);
346
347
  }
347
- // Otherwise, show initial loading state
348
348
  return Result.pending();
349
349
  });
350
350
  fetchStable(context)
@@ -42,11 +42,13 @@ export function useDetectedDataSources(
42
42
  dialects: [...connections.values()].map((connection) =>
43
43
  connection.dialect.toLowerCase(),
44
44
  ),
45
- storageProtocols: storageNamespaces.map((namespace) =>
46
- namespace.protocol.toLowerCase(),
45
+ storageProtocols: new Set(
46
+ storageNamespaces.map((namespace) => namespace.protocol.toLowerCase()),
47
47
  ),
48
- storageBackendTypes: storageNamespaces.map((namespace) =>
49
- namespace.backendType.toLowerCase(),
48
+ storageBackendTypes: new Set(
49
+ storageNamespaces.map((namespace) =>
50
+ namespace.backendType.toLowerCase(),
51
+ ),
50
52
  ),
51
53
  };
52
54