@marimo-team/islands 0.15.5 → 0.16.1

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 (217) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CBeIYi8p.js → ConnectedDataExplorerComponent-DyqLQGPc.js} +1567 -1544
  2. package/dist/{ImageComparisonComponent-Bk0a0xBq.js → ImageComparisonComponent-CQDGJfUA.js} +1 -1
  3. package/dist/{_baseUniq-utU5_Vu-.js → _baseUniq-B2Nna6Kt.js} +1 -1
  4. package/dist/{any-language-editor-PrUUh2lr.js → any-language-editor-D-wq0tOG.js} +1 -1
  5. package/dist/{architectureDiagram-W76B3OCA-D-vOp0UU.js → architectureDiagram-W76B3OCA-C6tdnMBf.js} +4 -4
  6. package/dist/assets/{worker-BcG8m3h5.js → worker-B0C57BK8.js} +40 -38
  7. package/dist/{blockDiagram-QIGZ2CNN-IG-z8q8A.js → blockDiagram-QIGZ2CNN-IagL8LCN.js} +5 -5
  8. package/dist/{c4Diagram-FPNF74CW-5AEXIX3t.js → c4Diagram-FPNF74CW-D3_lIWUP.js} +2 -2
  9. package/dist/{channel-ECVsTGGL.js → channel-DCJI_DKk.js} +1 -1
  10. package/dist/{chunk-4BX2VUAB-DfJcd9e-.js → chunk-4BX2VUAB-B2DrODwN.js} +1 -1
  11. package/dist/{chunk-55IACEB6-BwT8MejR.js → chunk-55IACEB6-BUWDsQ-t.js} +1 -1
  12. package/dist/{chunk-FMBD7UC4-DW7uxNR6.js → chunk-FMBD7UC4-BExPNFv1.js} +1 -1
  13. package/dist/{chunk-K7UQS3LO-BGn2ZPDQ.js → chunk-K7UQS3LO-Cixi-Yko.js} +4 -4
  14. package/dist/{chunk-QN33PNHL-BcIbOumv.js → chunk-QN33PNHL-B83MtvER.js} +1 -1
  15. package/dist/{chunk-QZHKN3VN-CMSnhk6x.js → chunk-QZHKN3VN-CXvbu85X.js} +1 -1
  16. package/dist/{chunk-TVAH2DTR-CZF2JRya.js → chunk-TVAH2DTR-CpiumCHg.js} +3 -3
  17. package/dist/{chunk-TZMSLE5B-BHzN_BY6.js → chunk-TZMSLE5B-DIzaZjcI.js} +1 -1
  18. package/dist/{classDiagram-v2-RKCZMP56-2H7MseyB.js → classDiagram-KNZD7YFC-DyN5HPdk.js} +2 -2
  19. package/dist/{classDiagram-KNZD7YFC-2H7MseyB.js → classDiagram-v2-RKCZMP56-DyN5HPdk.js} +2 -2
  20. package/dist/{clone-DKQcSK7N.js → clone-DrJYap2i.js} +1 -1
  21. package/dist/{cose-bilkent-S5V4N54A-CgvKFxTr.js → cose-bilkent-S5V4N54A-D39b4WrQ.js} +2 -2
  22. package/dist/{dagre-5GWH7T2D-VNFIipzt.js → dagre-5GWH7T2D-BLjRxDpS.js} +6 -6
  23. package/dist/{data-grid-overlay-editor-XdqkKCVx.js → data-grid-overlay-editor-DTALqerV.js} +2 -2
  24. package/dist/{diagram-N5W7TBWH-D1s8h-eH.js → diagram-N5W7TBWH-MM8AIKGR.js} +5 -5
  25. package/dist/{diagram-QEK2KX5R-DOa-AstT.js → diagram-QEK2KX5R-BZGarWuJ.js} +3 -3
  26. package/dist/{diagram-S2PKOQOG-CFZ-Y2zi.js → diagram-S2PKOQOG-CnPinN9Q.js} +3 -3
  27. package/dist/{dockerfile-zE-2DWBS.js → dockerfile-U8DnCJ4X.js} +1 -1
  28. package/dist/{erDiagram-AWTI2OKA-WxUYJfbS.js → erDiagram-AWTI2OKA-CvDVbxOO.js} +4 -4
  29. package/dist/{flowDiagram-PVAE7QVJ-dDZH2O1W.js → flowDiagram-PVAE7QVJ-C2uuBTZS.js} +5 -5
  30. package/dist/{ganttDiagram-OWAHRB6G-D3CCqPQq.js → ganttDiagram-OWAHRB6G-BEff10RF.js} +4 -4
  31. package/dist/{gitGraphDiagram-NY62KEGX-BHFylEwc.js → gitGraphDiagram-NY62KEGX-wggu0kb2.js} +4 -4
  32. package/dist/{glide-data-editor-D0aJSGV_.js → glide-data-editor-Bqh5_dzJ.js} +3 -3
  33. package/dist/{graph-BPGEu6c8.js → graph-DKpp_wzf.js} +3 -3
  34. package/dist/{index-HtOEKQ3O.js → index-4XruEJkp.js} +1 -1
  35. package/dist/{index-eDB61tLS.js → index-DW0BCGJE.js} +1 -1
  36. package/dist/{index-DotQhzoN.js → index-DdfF_cLK.js} +1 -1
  37. package/dist/{index-Bx2b23rX.js → index-DzJ_YPCG.js} +3 -3
  38. package/dist/{infoDiagram-STP46IZ2-DWhhqGPi.js → infoDiagram-STP46IZ2-DF7KW-Op.js} +2 -2
  39. package/dist/{journeyDiagram-BIP6EPQ6-CU8FpryL.js → journeyDiagram-BIP6EPQ6-B_jmhmqd.js} +3 -3
  40. package/dist/{kanban-definition-6OIFK2YF-CWhF_a4g.js → kanban-definition-6OIFK2YF-B-M9FTyw.js} +2 -2
  41. package/dist/{layout-DGonEvAZ.js → layout-C4oVYZZD.js} +4 -4
  42. package/dist/{linear-Cww2a6nQ.js → linear-C-HCGr0T.js} +1 -1
  43. package/dist/{main-Bc0LY9fB.js → main-B9x2-9f2.js} +93798 -93495
  44. package/dist/main.js +1 -1
  45. package/dist/{mermaid-DpJuOhRr.js → mermaid-BE4cM3Qs.js} +30 -30
  46. package/dist/{min-CFQjsG4L.js → min-DTpHJ698.js} +2 -2
  47. package/dist/{mindmap-definition-Q6HEUPPD-K513Ef1t.js → mindmap-definition-Q6HEUPPD-Cpd-hO1E.js} +3 -3
  48. package/dist/{number-overlay-editor-DuSchUfE.js → number-overlay-editor-CvURA2Ud.js} +2 -2
  49. package/dist/{pieDiagram-ADFJNKIX-DAIIUJJO.js → pieDiagram-ADFJNKIX-D9f_f6fn.js} +3 -3
  50. package/dist/{quadrantDiagram-LMRXKWRM-yuf-j7Os.js → quadrantDiagram-LMRXKWRM-DgllE7xw.js} +2 -2
  51. package/dist/{react-plotly-B378DZ9U.js → react-plotly-BU-JRJSi.js} +1 -1
  52. package/dist/{requirementDiagram-4UW4RH46-BBWvEl6q.js → requirementDiagram-4UW4RH46-Dk_G8eUb.js} +3 -3
  53. package/dist/{sankeyDiagram-GR3RE2ED-B_TwV-dS.js → sankeyDiagram-GR3RE2ED-BhLIhDc1.js} +1 -1
  54. package/dist/{sequenceDiagram-C3RYC4MD-BVC6lltp.js → sequenceDiagram-C3RYC4MD-DHoZdMFJ.js} +3 -3
  55. package/dist/{slides-component-CPX3S0Y9.js → slides-component-DXAgdf7K.js} +2 -2
  56. package/dist/{stateDiagram-KXAO66HF-BCU1tYTD.js → stateDiagram-KXAO66HF-C1Ie-7Xf.js} +4 -4
  57. package/dist/{stateDiagram-v2-UMBNRL4Z-BdvN6wTu.js → stateDiagram-v2-UMBNRL4Z--CRuIHtM.js} +2 -2
  58. package/dist/style.css +1 -1
  59. package/dist/{time-CSIip6fV.js → time-yQjlGPwa.js} +2 -2
  60. package/dist/{timeline-definition-XQNQX7LJ-CCxCPNQI.js → timeline-definition-XQNQX7LJ-D_PjxB1B.js} +1 -1
  61. package/dist/{treemap-75Q7IDZK-Du6v0BzD.js → treemap-75Q7IDZK--NYqQjUZ.js} +134 -134
  62. package/dist/{vega-component-Da93sTnp.js → vega-component-CCUOMM5K.js} +2 -2
  63. package/dist/{xychartDiagram-6GGTOJPD-Oq6xaZKR.js → xychartDiagram-6GGTOJPD-WLKsEnzs.js} +2 -2
  64. package/package.json +10 -5
  65. package/src/__tests__/mocks.ts +43 -0
  66. package/src/components/app-config/user-config-form.tsx +78 -1
  67. package/src/components/chat/acp/__tests__/__snapshots__/prompt.test.ts.snap +116 -65
  68. package/src/components/chat/acp/__tests__/atoms.test.ts +1 -1
  69. package/src/components/chat/acp/__tests__/context-utils.test.ts +222 -0
  70. package/src/components/chat/acp/__tests__/prompt.test.ts +1 -1
  71. package/src/components/chat/acp/__tests__/state.test.ts +38 -42
  72. package/src/components/chat/acp/agent-docs.tsx +33 -6
  73. package/src/components/chat/acp/agent-panel.css +0 -18
  74. package/src/components/chat/acp/agent-panel.tsx +394 -72
  75. package/src/components/chat/acp/agent-selector.tsx +7 -1
  76. package/src/components/chat/acp/blocks.tsx +40 -10
  77. package/src/components/chat/acp/common.tsx +10 -2
  78. package/src/components/chat/acp/context-utils.ts +127 -0
  79. package/src/components/chat/acp/prompt.ts +96 -53
  80. package/src/components/chat/acp/state.ts +1 -1
  81. package/src/components/chat/acp/types.ts +8 -0
  82. package/src/components/chat/chat-panel.tsx +28 -89
  83. package/src/components/chat/chat-utils.ts +127 -1
  84. package/src/components/chat/markdown-renderer.css +39 -0
  85. package/src/components/chat/markdown-renderer.tsx +12 -47
  86. package/src/components/chat/tool-call-accordion.tsx +148 -26
  87. package/src/components/data-table/SearchBar.tsx +8 -7
  88. package/src/components/data-table/__tests__/column_formatting.test.ts +50 -35
  89. package/src/components/data-table/__tests__/data-table.test.tsx +39 -1
  90. package/src/components/data-table/cell-hover-template/feature.ts +14 -0
  91. package/src/components/data-table/cell-hover-template/types.ts +11 -0
  92. package/src/components/data-table/charts/components/form-fields.tsx +41 -37
  93. package/src/components/data-table/charts/forms/common-chart.tsx +2 -2
  94. package/src/components/data-table/column-explorer-panel/column-explorer.tsx +5 -2
  95. package/src/components/data-table/column-formatting/feature.ts +62 -29
  96. package/src/components/data-table/column-formatting/types.ts +1 -0
  97. package/src/components/data-table/column-header.tsx +3 -1
  98. package/src/components/data-table/column-summary/chart-spec-model.tsx +24 -7
  99. package/src/components/data-table/column-summary/column-summary.tsx +18 -9
  100. package/src/components/data-table/columns.tsx +42 -18
  101. package/src/components/data-table/data-table.tsx +10 -2
  102. package/src/components/data-table/date-popover.tsx +85 -75
  103. package/src/components/data-table/filter-pills.tsx +14 -9
  104. package/src/components/data-table/header-items.tsx +5 -1
  105. package/src/components/data-table/pagination.tsx +20 -13
  106. package/src/components/data-table/renderers.tsx +28 -0
  107. package/src/components/data-table/row-viewer-panel/row-viewer.tsx +10 -8
  108. package/src/components/datasources/column-preview.tsx +6 -2
  109. package/src/components/datasources/datasources.tsx +8 -12
  110. package/src/components/editor/Cell.tsx +6 -0
  111. package/src/components/editor/actions/name-cell-input.tsx +6 -1
  112. package/src/components/editor/actions/useCellActionButton.tsx +3 -1
  113. package/src/components/editor/ai/__tests__/completion-utils.test.ts +178 -1
  114. package/src/components/editor/ai/add-cell-with-ai.tsx +68 -66
  115. package/src/components/editor/ai/ai-completion-editor.tsx +29 -26
  116. package/src/components/editor/ai/completion-handlers.tsx +44 -6
  117. package/src/components/editor/ai/completion-utils.ts +92 -0
  118. package/src/components/editor/ai/transport/chat-transport.tsx +39 -0
  119. package/src/components/editor/cell/CellStatus.tsx +23 -20
  120. package/src/components/editor/cell/CreateCellButton.tsx +3 -4
  121. package/src/components/editor/cell/StagedAICell.tsx +51 -0
  122. package/src/components/editor/cell/cell-actions.tsx +2 -1
  123. package/src/components/editor/cell/code/language-toggle.tsx +3 -4
  124. package/src/components/editor/chrome/wrapper/footer-items/machine-stats.tsx +39 -28
  125. package/src/components/editor/controls/notebook-menu-dropdown.tsx +4 -2
  126. package/src/components/editor/file-tree/requesting-tree.tsx +14 -8
  127. package/src/components/editor/renderers/CellArray.tsx +3 -4
  128. package/src/components/editor/renderers/slides-layout/slides-layout.tsx +3 -3
  129. package/src/components/editor/renderers/slides-layout/types.ts +1 -0
  130. package/src/components/pages/home-page.tsx +4 -1
  131. package/src/components/slides/slides-component.tsx +1 -1
  132. package/src/components/slides/slides.css +6 -0
  133. package/src/components/terminal/__tests__/state.test.ts +207 -0
  134. package/src/components/terminal/hooks.ts +41 -0
  135. package/src/components/terminal/state.ts +75 -0
  136. package/src/components/terminal/terminal.tsx +334 -13
  137. package/src/components/terminal/theme.tsx +57 -0
  138. package/src/components/tracing/tracing-spec.ts +5 -4
  139. package/src/components/ui/range-slider.tsx +4 -2
  140. package/src/components/ui/slider.tsx +3 -1
  141. package/src/components/variables/variables-table.tsx +3 -0
  142. package/src/core/MarimoApp.tsx +9 -6
  143. package/src/core/ai/__tests__/staged-cells.test.ts +356 -0
  144. package/src/core/ai/context/__tests__/registry.test.ts +6 -4
  145. package/src/core/ai/context/providers/cell-output.ts +3 -2
  146. package/src/core/ai/context/providers/error.ts +3 -1
  147. package/src/core/ai/context/providers/file.ts +7 -2
  148. package/src/core/ai/context/providers/tables.ts +3 -2
  149. package/src/core/ai/context/providers/variable.ts +6 -4
  150. package/src/core/ai/staged-cells.ts +241 -0
  151. package/src/core/cells/__tests__/add-missing-import.test.ts +67 -22
  152. package/src/core/cells/add-missing-import.ts +24 -7
  153. package/src/core/cells/cells.ts +27 -28
  154. package/src/core/cells/logs.ts +1 -1
  155. package/src/core/codemirror/find-replace/search-highlight.ts +3 -1
  156. package/src/core/codemirror/language/LanguageAdapters.ts +9 -3
  157. package/src/core/codemirror/lsp/federated-lsp.ts +1 -1
  158. package/src/core/codemirror/lsp/notebook-lsp.ts +8 -2
  159. package/src/core/codemirror/readonly/__tests__/extension.test.ts +1 -1
  160. package/src/core/codemirror/rtc/loro/awareness.ts +52 -17
  161. package/src/core/codemirror/rtc/loro/sync.ts +12 -4
  162. package/src/core/config/config-schema.ts +1 -0
  163. package/src/core/config/config.ts +4 -0
  164. package/src/core/hotkeys/hotkeys.ts +8 -4
  165. package/src/core/i18n/__tests__/locale-provider.test.tsx +176 -0
  166. package/src/core/i18n/locale-provider.tsx +35 -0
  167. package/src/core/i18n/with-locale.tsx +12 -0
  168. package/src/core/islands/components/web-components.tsx +13 -10
  169. package/src/core/islands/main.ts +2 -2
  170. package/src/core/kernel/RuntimeState.ts +4 -1
  171. package/src/core/kernel/messages.ts +8 -12
  172. package/src/core/network/DeferredRequestRegistry.ts +16 -4
  173. package/src/core/runtime/runtime.ts +5 -4
  174. package/src/core/saving/__tests__/filename.test.ts +37 -0
  175. package/src/core/static/__tests__/download-html.test.ts +43 -1
  176. package/src/core/wasm/bridge.ts +5 -1
  177. package/src/core/wasm/store.ts +4 -1
  178. package/src/core/wasm/worker/message-buffer.ts +3 -2
  179. package/src/core/websocket/types.ts +22 -16
  180. package/src/core/websocket/useMarimoWebSocket.tsx +2 -2
  181. package/src/css/app/Cell.css +11 -0
  182. package/src/hooks/useFormatting.ts +97 -0
  183. package/src/hooks/useTimer.ts +8 -5
  184. package/src/plugins/core/RenderHTML.tsx +36 -2
  185. package/src/plugins/core/__test__/RenderHTML.test.ts +72 -0
  186. package/src/plugins/core/registerReactComponent.tsx +44 -10
  187. package/src/plugins/impl/DataTablePlugin.tsx +4 -0
  188. package/src/plugins/impl/FileBrowserPlugin.tsx +8 -2
  189. package/src/plugins/impl/RangeSliderPlugin.tsx +5 -3
  190. package/src/plugins/impl/SliderPlugin.tsx +3 -1
  191. package/src/plugins/impl/anywidget/model.ts +16 -5
  192. package/src/plugins/impl/data-editor/types.ts +7 -5
  193. package/src/plugins/impl/data-explorer/components/column-summary.tsx +20 -13
  194. package/src/plugins/impl/panel/utils.ts +6 -4
  195. package/src/plugins/layout/OutlinePlugin.tsx +69 -0
  196. package/src/plugins/layout/StatPlugin.tsx +4 -1
  197. package/src/plugins/plugins.ts +2 -0
  198. package/src/stories/cell.stories.tsx +1 -1
  199. package/src/stories/layout/vertical/one-column.stories.tsx +1 -1
  200. package/src/utils/__tests__/cell-urls.test.ts +29 -0
  201. package/src/utils/__tests__/dates.test.ts +45 -24
  202. package/src/utils/__tests__/filenames.test.ts +18 -0
  203. package/src/utils/__tests__/numbers.test.ts +42 -30
  204. package/src/utils/__tests__/once.test.ts +187 -0
  205. package/src/utils/__tests__/path.test.ts +38 -0
  206. package/src/utils/__tests__/urls.test.ts +56 -1
  207. package/src/utils/dates.ts +15 -10
  208. package/src/utils/edit-distance.ts +8 -6
  209. package/src/utils/errors.ts +9 -0
  210. package/src/utils/id-tree.tsx +21 -10
  211. package/src/utils/localStorage.ts +13 -4
  212. package/src/utils/numbers.ts +11 -11
  213. package/src/utils/once.ts +32 -0
  214. package/src/utils/paths.ts +4 -1
  215. package/src/utils/pluralize.ts +12 -5
  216. package/src/utils/python-poet/poet.ts +30 -15
  217. package/src/utils/time.ts +5 -1
@@ -0,0 +1,176 @@
1
+ /* Copyright 2024 Marimo. All rights reserved. */
2
+
3
+ import { cleanup, render } from "@testing-library/react";
4
+ import { createStore, Provider } from "jotai";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+ import { userConfigAtom } from "@/core/config/config";
7
+ import { parseUserConfig } from "@/core/config/config-schema";
8
+ import { LocaleProvider } from "../locale-provider";
9
+
10
+ // Mock navigator.language with a getter
11
+ let mockNavigatorLanguage: string | undefined;
12
+
13
+ Object.defineProperty(window, "navigator", {
14
+ value: {
15
+ get language() {
16
+ return mockNavigatorLanguage;
17
+ },
18
+ },
19
+ writable: true,
20
+ });
21
+
22
+ // Mock react-aria-components I18nProvider
23
+ vi.mock("react-aria-components", () => ({
24
+ I18nProvider: ({
25
+ children,
26
+ locale,
27
+ }: {
28
+ children: React.ReactNode;
29
+ locale: string;
30
+ }) => (
31
+ <div data-testid="i18n-provider" data-locale={locale}>
32
+ {children}
33
+ </div>
34
+ ),
35
+ }));
36
+
37
+ describe("LocaleProvider", () => {
38
+ beforeEach(() => {
39
+ // Reset the mock before each test
40
+ mockNavigatorLanguage = undefined;
41
+ });
42
+
43
+ afterEach(() => {
44
+ cleanup();
45
+ // Clear all mocks after each test
46
+ mockNavigatorLanguage = undefined;
47
+ vi.clearAllMocks();
48
+ });
49
+
50
+ it("should render I18nProvider without locale when locale is null", () => {
51
+ const store = createStore();
52
+ const config = parseUserConfig({ display: { locale: null } });
53
+ store.set(userConfigAtom, config);
54
+
55
+ const { getByTestId } = render(
56
+ <Provider store={store}>
57
+ <LocaleProvider>
58
+ <div>Test content</div>
59
+ </LocaleProvider>
60
+ </Provider>,
61
+ );
62
+
63
+ const i18nProvider = getByTestId("i18n-provider");
64
+ expect(i18nProvider).toBeInTheDocument();
65
+ expect(i18nProvider.dataset.locale).toBe(undefined);
66
+ expect(i18nProvider).toHaveTextContent("Test content");
67
+ });
68
+
69
+ it("should render I18nProvider without locale when locale is undefined", () => {
70
+ const store = createStore();
71
+ const config = parseUserConfig({ display: { locale: undefined } });
72
+ store.set(userConfigAtom, config);
73
+
74
+ const { getByTestId } = render(
75
+ <Provider store={store}>
76
+ <LocaleProvider>
77
+ <div>Test content</div>
78
+ </LocaleProvider>
79
+ </Provider>,
80
+ );
81
+
82
+ const i18nProvider = getByTestId("i18n-provider");
83
+ expect(i18nProvider).toBeInTheDocument();
84
+ expect(i18nProvider.dataset.locale).toBe(undefined);
85
+ expect(i18nProvider).toHaveTextContent("Test content");
86
+ });
87
+
88
+ it("should render I18nProvider with locale when locale is provided", () => {
89
+ const store = createStore();
90
+ const testLocale = "es-ES";
91
+ const config = parseUserConfig({ display: { locale: testLocale } });
92
+ store.set(userConfigAtom, config);
93
+
94
+ const { getByTestId } = render(
95
+ <Provider store={store}>
96
+ <LocaleProvider>
97
+ <div>Test content</div>
98
+ </LocaleProvider>
99
+ </Provider>,
100
+ );
101
+
102
+ const i18nProvider = getByTestId("i18n-provider");
103
+ expect(i18nProvider).toBeInTheDocument();
104
+ expect(i18nProvider.dataset.locale).toBe(testLocale);
105
+ expect(i18nProvider).toHaveTextContent("Test content");
106
+ });
107
+
108
+ it("should render I18nProvider with different locale values", () => {
109
+ const testCases = ["en-US", "fr-FR", "de-DE", "ja-JP"];
110
+
111
+ testCases.forEach((locale) => {
112
+ const store = createStore();
113
+ const config = parseUserConfig({ display: { locale } });
114
+ store.set(userConfigAtom, config);
115
+
116
+ const { getByTestId } = render(
117
+ <Provider store={store}>
118
+ <LocaleProvider>
119
+ <div>Test content for {locale}</div>
120
+ </LocaleProvider>
121
+ </Provider>,
122
+ );
123
+
124
+ const i18nProvider = getByTestId("i18n-provider");
125
+ expect(i18nProvider.dataset.locale).toBe(locale);
126
+ expect(i18nProvider).toHaveTextContent(`Test content for ${locale}`);
127
+
128
+ // Clean up after each iteration
129
+ cleanup();
130
+ });
131
+ });
132
+
133
+ it("should render children correctly", () => {
134
+ const store = createStore();
135
+ const config = parseUserConfig({ display: { locale: "en-US" } });
136
+ store.set(userConfigAtom, config);
137
+
138
+ const { getByText, getByRole } = render(
139
+ <Provider store={store}>
140
+ <LocaleProvider>
141
+ <div>
142
+ <h1>Test Heading</h1>
143
+ <p>Test paragraph</p>
144
+ <button type="button">Test Button</button>
145
+ </div>
146
+ </LocaleProvider>
147
+ </Provider>,
148
+ );
149
+
150
+ expect(getByText("Test Heading")).toBeInTheDocument();
151
+ expect(getByText("Test paragraph")).toBeInTheDocument();
152
+ expect(getByRole("button", { name: "Test Button" })).toBeInTheDocument();
153
+ });
154
+
155
+ it("should auto-detect locale when no locale is set in config", () => {
156
+ mockNavigatorLanguage = "de-DE";
157
+
158
+ const store = createStore();
159
+ const config = parseUserConfig({});
160
+ store.set(userConfigAtom, config);
161
+
162
+ const { getByTestId } = render(
163
+ <Provider store={store}>
164
+ <LocaleProvider>
165
+ <div>Test content</div>
166
+ </LocaleProvider>
167
+ </Provider>,
168
+ );
169
+
170
+ const i18nProvider = getByTestId("i18n-provider");
171
+ expect(i18nProvider).toBeInTheDocument();
172
+ // When no locale is specified in config, it should use navigator.language
173
+ expect(i18nProvider.dataset.locale).toBe("de-DE");
174
+ expect(i18nProvider).toHaveTextContent("Test content");
175
+ });
176
+ });
@@ -0,0 +1,35 @@
1
+ /* Copyright 2024 Marimo. All rights reserved. */
2
+
3
+ import { useAtomValue } from "jotai";
4
+ import type { ReactNode } from "react";
5
+ import { I18nProvider } from "react-aria-components";
6
+ import { localeAtom } from "@/core/config/config";
7
+
8
+ interface LocaleProviderProps {
9
+ children: ReactNode;
10
+ }
11
+
12
+ export const LocaleProvider = ({ children }: LocaleProviderProps) => {
13
+ const locale = useAtomValue(localeAtom);
14
+
15
+ return <I18nProvider locale={safeLocale(locale)}>{children}</I18nProvider>;
16
+ };
17
+
18
+ function safeLocale(locale: string | null | undefined) {
19
+ if (!locale) {
20
+ return navigator.language;
21
+ }
22
+ if (isValidLocale(locale)) {
23
+ return locale;
24
+ }
25
+ return navigator.language;
26
+ }
27
+
28
+ function isValidLocale(locale: string) {
29
+ try {
30
+ new Intl.NumberFormat(locale);
31
+ return true;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
@@ -0,0 +1,12 @@
1
+ /* Copyright 2024 Marimo. All rights reserved. */
2
+
3
+ import { useLocale } from "react-aria";
4
+
5
+ export const WithLocale = ({
6
+ children,
7
+ }: {
8
+ children: (locale: string) => React.ReactNode;
9
+ }) => {
10
+ const { locale } = useLocale();
11
+ return children(locale);
12
+ };
@@ -7,6 +7,7 @@ import { ErrorBoundary } from "@/components/editor/boundary/ErrorBoundary";
7
7
  import { TooltipProvider } from "@/components/ui/tooltip";
8
8
  import { notebookAtom } from "@/core/cells/cells";
9
9
  import { UI_ELEMENT_REGISTRY } from "@/core/dom/uiregistry";
10
+ import { LocaleProvider } from "@/core/i18n/locale-provider";
10
11
  import { renderHTML } from "@/plugins/core/RenderHTML";
11
12
  import { invariant } from "@/utils/invariant";
12
13
  import type { CellId } from "../../cells/ids";
@@ -80,16 +81,18 @@ export class MarimoIslandElement extends HTMLElement {
80
81
  this.root?.render(
81
82
  <ErrorBoundary>
82
83
  <Provider store={store}>
83
- <TooltipProvider>
84
- <MarimoOutputWrapper
85
- cellId={this.cellId}
86
- codeCallback={codeCallback}
87
- alwaysShowRun={alwaysShowRun}
88
- >
89
- {initialHtml}
90
- </MarimoOutputWrapper>
91
- {editor}
92
- </TooltipProvider>
84
+ <LocaleProvider>
85
+ <TooltipProvider>
86
+ <MarimoOutputWrapper
87
+ cellId={this.cellId}
88
+ codeCallback={codeCallback}
89
+ alwaysShowRun={alwaysShowRun}
90
+ >
91
+ {initialHtml}
92
+ </MarimoOutputWrapper>
93
+ {editor}
94
+ </TooltipProvider>
95
+ </LocaleProvider>
93
96
  </Provider>
94
97
  </ErrorBoundary>,
95
98
  );
@@ -103,7 +103,7 @@ export async function initialize() {
103
103
  // Consume messages from the kernel
104
104
  IslandsPyodideBridge.INSTANCE.consumeMessages((message) => {
105
105
  const msg = jsonParseWithSpecialChar(message);
106
- switch (msg.op) {
106
+ switch (msg.data.op) {
107
107
  case "banner":
108
108
  case "missing-package-alert":
109
109
  case "installing-package-alert":
@@ -185,7 +185,7 @@ export async function initialize() {
185
185
  case "reconnected":
186
186
  return;
187
187
  default:
188
- logNever(msg);
188
+ logNever(msg.data);
189
189
  }
190
190
  });
191
191
 
@@ -29,8 +29,11 @@ export class RuntimeState {
29
29
  * ObjectIds of UIElements whose values need to be updated in the kernel
30
30
  */
31
31
  private _sendComponentValues: RunRequests["sendComponentValues"] | undefined;
32
+ private uiElementRegistry: UIElementRegistry;
32
33
 
33
- constructor(private uiElementRegistry: UIElementRegistry) {}
34
+ constructor(uiElementRegistry: UIElementRegistry) {
35
+ this.uiElementRegistry = uiElementRegistry;
36
+ }
34
37
 
35
38
  private get sendComponentValues(): RunRequests["sendComponentValues"] {
36
39
  if (!this._sendComponentValues) {
@@ -40,21 +40,17 @@ export type SQLTableListPreview =
40
40
  OperationMessageData<"sql-table-list-preview">;
41
41
  export type SecretKeysResult = OperationMessageData<"secret-keys-result">;
42
42
  export type StartupLogs = OperationMessageData<"startup-logs">;
43
- export type MessageOperation = schemas["KnownUnions"]["operation"];
43
+ export type CellMessage = OperationMessageData<"cell-op">;
44
+ export type Capabilities = OperationMessageData<"kernel-ready">["capabilities"];
44
45
 
45
- export type OperationMessageType = MessageOperation["op"];
46
- export type OperationMessage = {
47
- [Type in OperationMessageType]: {
48
- op: Type;
49
- data: Omit<Extract<MessageOperation, { op: Type }>, "op">;
50
- };
51
- }[OperationMessageType];
46
+ export type MessageOperationUnion = schemas["KnownUnions"]["operation"];
52
47
 
53
- export type CellMessage = OperationMessageData<"cell-op">;
48
+ export type OperationMessageType = MessageOperationUnion["op"];
49
+ export interface OperationMessage {
50
+ data: MessageOperationUnion;
51
+ }
54
52
 
55
53
  export type OperationMessageData<T extends OperationMessageType> = Omit<
56
- Extract<MessageOperation, { op: T }>,
54
+ Extract<MessageOperationUnion, { op: T }>,
57
55
  "op"
58
56
  >;
59
-
60
- export type Capabilities = OperationMessageData<"kernel-ready">["capabilities"];
@@ -18,17 +18,29 @@ export const RequestId = {
18
18
  */
19
19
  export class DeferredRequestRegistry<REQ, RES> {
20
20
  public requests = new Map<RequestId, Deferred<RES>>();
21
+ public operation: string;
22
+ private makeRequest: (id: RequestId, req: REQ) => Promise<void>;
23
+ private opts: {
24
+ /**
25
+ * Resolve existing requests with an empty response.
26
+ */
27
+ resolveExistingRequests?: () => RES;
28
+ };
21
29
 
22
30
  constructor(
23
- public operation: string,
24
- private makeRequest: (id: RequestId, req: REQ) => Promise<void>,
25
- private opts: {
31
+ operation: string,
32
+ makeRequest: (id: RequestId, req: REQ) => Promise<void>,
33
+ opts: {
26
34
  /**
27
35
  * Resolve existing requests with an empty response.
28
36
  */
29
37
  resolveExistingRequests?: () => RES;
30
38
  } = {},
31
- ) {}
39
+ ) {
40
+ this.operation = operation;
41
+ this.makeRequest = makeRequest;
42
+ this.opts = opts;
43
+ }
32
44
 
33
45
  async request(opts: REQ): Promise<RES> {
34
46
  if (this.opts.resolveExistingRequests) {
@@ -10,11 +10,12 @@ import type { RuntimeConfig } from "./types";
10
10
 
11
11
  export class RuntimeManager {
12
12
  private initialHealthyCheck = new Deferred<void>();
13
+ private config: RuntimeConfig;
14
+ private lazy: boolean;
13
15
 
14
- constructor(
15
- private config: RuntimeConfig,
16
- private lazy = false,
17
- ) {
16
+ constructor(config: RuntimeConfig, lazy = false) {
17
+ this.config = config;
18
+ this.lazy = lazy;
18
19
  // Validate the URL on construction
19
20
  try {
20
21
  new URL(this.config.url);
@@ -0,0 +1,37 @@
1
+ /* Copyright 2024 Marimo. All rights reserved. */
2
+
3
+ import { describe, expect, it } from "vitest";
4
+ import { EDGE_CASE_FILENAMES } from "../../../__tests__/mocks";
5
+ import { Paths } from "../../../utils/paths";
6
+
7
+ describe("filename handling logic", () => {
8
+ it.each(EDGE_CASE_FILENAMES)(
9
+ "should extract basename correctly for document title: %s",
10
+ (filename) => {
11
+ const basename = Paths.basename(filename);
12
+ expect(basename).toBe(filename); // Since no path separator
13
+ },
14
+ );
15
+
16
+ it("should handle full paths with unicode filenames", () => {
17
+ EDGE_CASE_FILENAMES.forEach((filename) => {
18
+ const fullPath = `/path/to/${filename}`;
19
+
20
+ const basename = Paths.basename(fullPath);
21
+ expect(basename).toBe(filename);
22
+ });
23
+ });
24
+
25
+ it("should handle document title setting with unicode", () => {
26
+ EDGE_CASE_FILENAMES.forEach((filename) => {
27
+ const originalTitle = document.title;
28
+
29
+ // In case this does any conversions, we want to simulate reading/writing the title
30
+ document.title = filename;
31
+ expect(document.title).toBe(filename);
32
+
33
+ // Restore
34
+ document.title = originalTitle;
35
+ });
36
+ });
37
+ });
@@ -1,5 +1,8 @@
1
1
  /* Copyright 2024 Marimo. All rights reserved. */
2
- import { describe, expect, it } from "vitest";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { EDGE_CASE_FILENAMES } from "../../../__tests__/mocks";
4
+ import { Filenames } from "../../../utils/filenames";
5
+ import { Paths } from "../../../utils/paths";
3
6
  import { visibleForTesting } from "../download-html";
4
7
 
5
8
  const { updateAssetUrl } = visibleForTesting;
@@ -39,3 +42,42 @@ describe("updateAssetUrl", () => {
39
42
  expect(updateAssetUrl(existingUrl, assetBaseUrl)).toBe(existingUrl);
40
43
  });
41
44
  });
45
+
46
+ describe("filename handling for downloads", () => {
47
+ it.each(EDGE_CASE_FILENAMES)(
48
+ "should handle edge case filenames in download operations: %s",
49
+ (filename) => {
50
+ // Test that basename extraction works correctly for downloads
51
+ const basename = Paths.basename(filename);
52
+ expect(basename).toBe(filename);
53
+
54
+ // Test filename conversion for HTML downloads
55
+ const htmlFilename = Filenames.toHTML(filename);
56
+ expect(htmlFilename).toMatch(/\.html$/);
57
+ expect(htmlFilename).toContain(Filenames.withoutExtension(filename));
58
+
59
+ // Ensure unicode and spaces are preserved in basename
60
+ const withoutExt = Filenames.withoutExtension(filename);
61
+ expect(withoutExt).not.toBe("");
62
+ expect(typeof withoutExt).toBe("string");
63
+ },
64
+ );
65
+
66
+ it("should handle blob download filename generation", () => {
67
+ // Mock URL.createObjectURL for blob testing
68
+ const mockCreateObjectURL = vi.fn().mockReturnValue("blob:mock-url");
69
+ global.URL.createObjectURL = mockCreateObjectURL;
70
+
71
+ EDGE_CASE_FILENAMES.forEach((filename) => {
72
+ const htmlFilename = Filenames.toHTML(filename);
73
+
74
+ // Verify blob can be created with unicode filenames
75
+ expect(() => new Blob(["test"], { type: "text/html" })).not.toThrow();
76
+ expect(htmlFilename).toMatch(/\.html$/);
77
+
78
+ // Verify filename maintains unicode characters
79
+ const baseName = Filenames.withoutExtension(filename);
80
+ expect(htmlFilename).toContain(baseName);
81
+ });
82
+ });
83
+ });
@@ -576,7 +576,11 @@ export class PyodideWebsocket implements IReconnectingWebSocket {
576
576
  messageSubscriptions = new Set<(event: MessageEvent) => void>();
577
577
  errorSubscriptions = new Set<(event: Event) => void>();
578
578
 
579
- constructor(private bridge: Pick<PyodideBridge, "consumeMessages">) {}
579
+ private bridge: Pick<PyodideBridge, "consumeMessages">;
580
+
581
+ constructor(bridge: Pick<PyodideBridge, "consumeMessages">) {
582
+ this.bridge = bridge;
583
+ }
580
584
 
581
585
  private consumeMessages() {
582
586
  this.bridge.consumeMessages((message) => {
@@ -87,7 +87,10 @@ const emptyFileStore: FileStore = {
87
87
  };
88
88
 
89
89
  export class CompositeFileStore implements FileStore {
90
- constructor(private stores: FileStore[]) {}
90
+ private stores: FileStore[];
91
+ constructor(stores: FileStore[]) {
92
+ this.stores = stores;
93
+ }
91
94
 
92
95
  insert(index: number, store: FileStore) {
93
96
  this.stores.splice(index, 0, store);
@@ -7,8 +7,9 @@
7
7
  export class MessageBuffer<T> {
8
8
  private buffer: T[];
9
9
  private started = false;
10
-
11
- constructor(private onMessage: (data: T) => void) {
10
+ private onMessage: (data: T) => void;
11
+ constructor(onMessage: (data: T) => void) {
12
+ this.onMessage = onMessage;
12
13
  this.buffer = [];
13
14
  }
14
15
 
@@ -2,22 +2,28 @@
2
2
 
3
3
  import type ReconnectingWebSocket from "partysocket/ws";
4
4
 
5
- export enum WebSocketState {
6
- CONNECTING = "CONNECTING",
7
- OPEN = "CONNECTED",
8
- CLOSING = "CLOSING",
9
- CLOSED = "CLOSED",
10
- }
11
-
12
- export enum WebSocketClosedReason {
13
- KERNEL_DISCONNECTED = "KERNEL_DISCONNECTED",
14
- ALREADY_RUNNING = "ALREADY_RUNNING",
15
- MALFORMED_QUERY = "MALFORMED_QUERY",
16
- }
5
+ export const WebSocketState = {
6
+ CONNECTING: "CONNECTING",
7
+ OPEN: "OPEN",
8
+ CLOSING: "CLOSING",
9
+ CLOSED: "CLOSED",
10
+ } as const;
11
+
12
+ export type WebSocketState =
13
+ (typeof WebSocketState)[keyof typeof WebSocketState];
14
+
15
+ export const WebSocketClosedReason = {
16
+ KERNEL_DISCONNECTED: "KERNEL_DISCONNECTED",
17
+ ALREADY_RUNNING: "ALREADY_RUNNING",
18
+ MALFORMED_QUERY: "MALFORMED_QUERY",
19
+ } as const;
20
+
21
+ export type WebSocketClosedReason =
22
+ (typeof WebSocketClosedReason)[keyof typeof WebSocketClosedReason];
17
23
 
18
24
  export type ConnectionStatus =
19
25
  | {
20
- state: WebSocketState.CLOSED;
26
+ state: typeof WebSocketState.CLOSED;
21
27
  code: WebSocketClosedReason;
22
28
  /**
23
29
  * Human-readable reason for closing the connection.
@@ -31,9 +37,9 @@ export type ConnectionStatus =
31
37
  }
32
38
  | {
33
39
  state:
34
- | WebSocketState.CONNECTING
35
- | WebSocketState.OPEN
36
- | WebSocketState.CLOSING;
40
+ | typeof WebSocketState.CONNECTING
41
+ | typeof WebSocketState.OPEN
42
+ | typeof WebSocketState.CLOSING;
37
43
  };
38
44
 
39
45
  type PublicInterface<T> = {
@@ -86,7 +86,7 @@ export function useMarimoWebSocket(opts: {
86
86
 
87
87
  const handleMessage = (e: MessageEvent<JsonString<OperationMessage>>) => {
88
88
  const msg = jsonParseWithSpecialChar(e.data);
89
- switch (msg.op) {
89
+ switch (msg.data.op) {
90
90
  case "reload":
91
91
  reloadSafe();
92
92
  return;
@@ -267,7 +267,7 @@ export function useMarimoWebSocket(opts: {
267
267
  setCellIds({ cellIds: msg.data.cell_ids as CellId[] });
268
268
  return;
269
269
  default:
270
- logNever(msg);
270
+ logNever(msg.data);
271
271
  }
272
272
  };
273
273
 
@@ -23,6 +23,17 @@
23
23
  outline: none;
24
24
  }
25
25
 
26
+ &:has(.mo-ai-generated-cell)::before {
27
+ content: "";
28
+ position: absolute;
29
+ inset: 0;
30
+ border-radius: 10px;
31
+ pointer-events: none;
32
+ opacity: 0.5;
33
+ box-shadow: 0px 0px 10px 0px var(--cyan-9);
34
+ z-index: 1;
35
+ }
36
+
26
37
  &:focus-within {
27
38
  z-index: 20;
28
39
  }