@marimo-team/islands 0.23.10-dev8 → 0.23.10

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 (188) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CyV83R2m.js → ConnectedDataExplorerComponent-Z3RP7Vmm.js} +31 -31
  2. package/dist/{ErrorBoundary-rULOrC_p.js → ErrorBoundary-DpbaKVv7.js} +1 -1
  3. package/dist/{any-language-editor-CiES2a2h.js → any-language-editor-CAgFD4Kd.js} +20 -20
  4. package/dist/assets/__vite-browser-external-eshhtsgZ.js +1 -0
  5. package/dist/assets/worker-CC0Oul9k.js +73 -0
  6. package/dist/{chat-ui-K84W5ecY.js → chat-ui-C4-GMW9y.js} +77 -77
  7. package/dist/{check-DTbrK0zt.js → check-BCaJeT-J.js} +1 -1
  8. package/dist/{chunk-5FQGJX7Z-BNjes6Yx.js → chunk-5FQGJX7Z-BbqSm5gU.js} +1227 -1222
  9. package/dist/{code-block-37QAKDTI-m92Yc8pv.js → code-block-37QAKDTI-O6IH6wAi.js} +1 -1
  10. package/dist/{code-visibility-ChnpTxqU.js → code-visibility-DrFozVlm.js} +1943 -1223
  11. package/dist/{copy-5jQ_kGE1.js → copy-UqRYxiOg.js} +1 -1
  12. package/dist/dist-7QfXoMdB.js +5 -0
  13. package/dist/{dist-DgnE8F-r.js → dist-A2846XWO.js} +1 -1
  14. package/dist/dist-BEXXyZig.js +5 -0
  15. package/dist/{dist-B3pZ0Ab6.js → dist-BR_gyG9L.js} +3 -3
  16. package/dist/{dist-CcXxepx6.js → dist-BSAt6RhH.js} +27 -27
  17. package/dist/{dist-Bde4a2kU.js → dist-BY018Paw.js} +8 -8
  18. package/dist/dist-BYj57OV4.js +5 -0
  19. package/dist/{dist-CUCNs1ja.js → dist-BaoDKvdy.js} +2 -2
  20. package/dist/{dist-Cy1WxgBD.js → dist-Bf7SHuNp.js} +5 -5
  21. package/dist/{dist-Bz_sYWbr.js → dist-Bk75fBZA.js} +2 -2
  22. package/dist/dist-BlSvQzNr.js +5 -0
  23. package/dist/{dist-C5VC_yzu.js → dist-BzEzfugY.js} +1 -1
  24. package/dist/dist-CCBlxAgS.js +8 -0
  25. package/dist/dist-CIDTVIUf.js +5 -0
  26. package/dist/{dist-CLUtPrdy.js → dist-CIYBwstr.js} +1 -1
  27. package/dist/{dist-BotSqB48.js → dist-C_Y3oV3C.js} +12 -12
  28. package/dist/{dist-BTfv03uy.js → dist-CcWX6tmx.js} +2 -2
  29. package/dist/{dist-BhM8gdSO.js → dist-CoXAujgg.js} +4 -4
  30. package/dist/{dist-4j4c7bjm.js → dist-CpxNdDkw.js} +3 -3
  31. package/dist/dist-CqQyhAM8.js +8 -0
  32. package/dist/dist-CwRu2Xzh.js +5 -0
  33. package/dist/{dist-BcuoonNH.js → dist-CxJDU6Bh.js} +9 -9
  34. package/dist/{dist-DxvORzUR.js → dist-D-W5ny5a.js} +8 -8
  35. package/dist/dist-D8CDTVgf.js +6 -0
  36. package/dist/dist-D8DNB0nO.js +8 -0
  37. package/dist/dist-DL6N_q-A.js +5 -0
  38. package/dist/{dist-BbbIBDiQ.js → dist-DMjWuVs8.js} +1 -1
  39. package/dist/dist-DOFbNV_b.js +8 -0
  40. package/dist/dist-DPrYzMY0.js +6 -0
  41. package/dist/{dist-h2c8sZvT.js → dist-DZORgqKY.js} +1 -1
  42. package/dist/{dist-B3P2fFpz.js → dist-DZo4nSS0.js} +14 -14
  43. package/dist/{dist-D4CewLk6.js → dist-Dax--nl9.js} +1 -1
  44. package/dist/{dist-DRfcqpxJ.js → dist-DgGbNavJ.js} +2 -2
  45. package/dist/{dist-C1BYNeCR.js → dist-Dk6PV_d3.js} +10 -10
  46. package/dist/{dist-fQ0ViXGs.js → dist-Dv_Y15yk.js} +107 -107
  47. package/dist/{dist-Bfwsv11D.js → dist-DyyjKEYf.js} +2 -2
  48. package/dist/{dist-p2qyWijU.js → dist-GZXUmt0b.js} +2 -2
  49. package/dist/{dist-CLJWPTX2.js → dist-LTU8Hdvn.js} +3 -3
  50. package/dist/{dist-DqAWR3CS.js → dist-M9Vag9Y0.js} +20 -20
  51. package/dist/{dist-DNdhYsgW.js → dist-U4F-tbMs.js} +79 -62
  52. package/dist/{dist-RqXTaiir.js → dist-abid3KgM.js} +11 -11
  53. package/dist/dist-cdmMjgsn.js +5 -0
  54. package/dist/dist-hT4QzYX-.js +1247 -0
  55. package/dist/{dist-luvabDEB.js → dist-t9Kf7xqC.js} +2 -2
  56. package/dist/{error-banner-5bz0L9hS.js → error-banner-CJXYJ6Sb.js} +11 -6
  57. package/dist/esm-BaH2eg5-.js +1171 -0
  58. package/dist/{esm-Duie8iU-.js → esm-ga2Bf3O2.js} +43 -43
  59. package/dist/{extends-CkydH1Q5.js → extends-D_hDsj6R.js} +4 -4
  60. package/dist/files/wasm-intro.py +11 -8
  61. package/dist/{formats-DHxc-FdY.js → formats-C4wO47tk.js} +1 -1
  62. package/dist/{glide-data-editor-CgIxTzAP.js → glide-data-editor-BPkCPs7L.js} +255 -255
  63. package/dist/{html-to-image-B93KtAVY.js → html-to-image-BwfcJJxA.js} +2412 -2282
  64. package/dist/{input-_2sjvfne.js → input-OdWHkobi.js} +263 -245
  65. package/dist/{label-LWtdw5i8.js → label-CC4ytI1X.js} +1 -1
  66. package/dist/main.js +7630 -7558
  67. package/dist/{mermaid-4DMBBIKO-DIdL224_.js → mermaid-4DMBBIKO-Eg3D4FVz.js} +1 -1
  68. package/dist/{mermaid-lXOw5Py9.js → mermaid-BotvIKg9.js} +4 -4
  69. package/dist/{process-output-BOXi9fnS.js → process-output-DsloEDna.js} +3 -3
  70. package/dist/{reveal-component-OrljHylW.js → reveal-component-3YQBHWuD.js} +698 -620
  71. package/dist/{spec-B96zNUEA.js → spec-X7FwLJni.js} +4 -4
  72. package/dist/{strings-Bu3vlb6W.js → strings-J57tzLr3.js} +47 -46
  73. package/dist/style.css +1 -1
  74. package/dist/{toDate-x-WRDCH7.js → toDate-d8RCRrRd.js} +2 -2
  75. package/dist/{tooltip-C5FYOpQc.js → tooltip-DpcyNkQ2.js} +2 -2
  76. package/dist/{types-CVvp1fKr.js → types-ChtMFmZ2.js} +1 -1
  77. package/dist/{useAsyncData-iRgKDT5s.js → useAsyncData-PonK__yh.js} +1 -1
  78. package/dist/{useDateFormatter-BA4FCquG.js → useDateFormatter-QB-3MpYr.js} +2 -2
  79. package/dist/{useDeepCompareMemoize-CkQ57VS2.js → useDeepCompareMemoize-D3NGWke6.js} +1 -1
  80. package/dist/{useLifecycle-BBO9PIph.js → useLifecycle-00mO3OSS.js} +2 -2
  81. package/dist/{useTheme-DHIrRQOe.js → useTheme-DEhDzATN.js} +1 -1
  82. package/dist/{vega-component-Dq-SH463.js → vega-component-DGPUhbDs.js} +8 -8
  83. package/dist/{zod-CoBiJ5v4.js → zod-aLSua2NL.js} +24 -23
  84. package/package.json +4 -3
  85. package/src/__tests__/setup.ts +16 -0
  86. package/src/components/chat/chat-display.tsx +2 -2
  87. package/src/components/data-table/TableBottomBar.tsx +1 -15
  88. package/src/components/data-table/TableTopBar.tsx +8 -13
  89. package/src/components/data-table/__tests__/TableBottomBar.test.tsx +6 -12
  90. package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +227 -0
  91. package/src/components/data-table/column-visibility-dropdown.tsx +204 -0
  92. package/src/components/data-table/data-table.tsx +1 -1
  93. package/src/components/data-table/filter-by-values-picker.tsx +39 -17
  94. package/src/components/data-table/filter-pills.tsx +1 -1
  95. package/src/components/datasources/__tests__/filter-empty.test.ts +183 -0
  96. package/src/components/datasources/datasources.tsx +92 -3
  97. package/src/components/editor/cell/code/cell-editor.tsx +70 -32
  98. package/src/components/editor/cell/code/code-placeholder.css +18 -0
  99. package/src/components/editor/cell/code/code-placeholder.tsx +47 -0
  100. package/src/components/editor/cell/code/language-toggle.tsx +7 -1
  101. package/src/components/editor/chrome/wrapper/app-chrome.tsx +97 -52
  102. package/src/components/editor/chrome/wrapper/lazy-panels.ts +91 -0
  103. package/src/components/editor/chrome/wrapper/sidebar.tsx +2 -0
  104. package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +9 -2
  105. package/src/components/editor/connections/storage/__tests__/as-code.test.ts +62 -27
  106. package/src/components/editor/connections/storage/add-storage-form.tsx +5 -1
  107. package/src/components/editor/connections/storage/as-code.ts +20 -6
  108. package/src/components/editor/documentation.css +35 -0
  109. package/src/components/editor/file-tree/file-explorer.tsx +8 -18
  110. package/src/components/editor/file-tree/tree-actions.tsx +46 -1
  111. package/src/components/editor/notebook-cell.tsx +3 -3
  112. package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +20 -0
  113. package/src/components/editor/renderers/slides-layout/types.ts +1 -0
  114. package/src/components/markdown/__tests__/markdown-renderer.test.tsx +43 -0
  115. package/src/components/markdown/markdown-renderer.tsx +24 -1
  116. package/src/components/slides/__tests__/minimap-actions.test.tsx +166 -0
  117. package/src/components/slides/__tests__/reveal-component.test.ts +425 -0
  118. package/src/components/slides/minimap.tsx +127 -10
  119. package/src/components/slides/reveal-component.tsx +287 -61
  120. package/src/components/slides/slide-cell-view.tsx +26 -2
  121. package/src/components/slides/slide-form.tsx +26 -4
  122. package/src/components/storage/__tests__/storage-inspector.test.ts +180 -0
  123. package/src/components/storage/storage-inspector.tsx +535 -81
  124. package/src/components/ui/combobox.tsx +51 -32
  125. package/src/components/ui/number-field.tsx +5 -1
  126. package/src/components/ui/reorderable-list.tsx +13 -0
  127. package/src/components/ui/select-core/__tests__/use-select-list.test.ts +294 -0
  128. package/src/components/ui/select-core/__tests__/utils.test.ts +222 -0
  129. package/src/components/ui/select-core/index.ts +16 -0
  130. package/src/components/ui/select-core/option-row.tsx +33 -0
  131. package/src/components/ui/select-core/render-slot.ts +20 -0
  132. package/src/components/ui/select-core/select-list.tsx +248 -0
  133. package/src/components/ui/select-core/types.ts +44 -0
  134. package/src/components/ui/select-core/use-select-list.ts +347 -0
  135. package/src/components/ui/select-core/utils.ts +121 -0
  136. package/src/components/ui/toast.tsx +1 -0
  137. package/src/core/cells/__tests__/document-changes.test.ts +2 -2
  138. package/src/core/cells/__tests__/focus.test.ts +45 -2
  139. package/src/core/cells/focus.ts +1 -1
  140. package/src/core/cells/scrollCellIntoView.ts +4 -0
  141. package/src/core/cells/utils.ts +1 -1
  142. package/src/core/codemirror/__tests__/editor-mount-scheduler.test.ts +143 -0
  143. package/src/core/codemirror/copilot/getCodes.ts +3 -1
  144. package/src/core/codemirror/editor-mount-scheduler.ts +101 -0
  145. package/src/core/codemirror/language/languages/python.ts +2 -0
  146. package/src/core/codemirror/language/languages/sql/utils.ts +3 -1
  147. package/src/core/codemirror/lsp/__tests__/markdown-renderer.test.ts +41 -0
  148. package/src/core/codemirror/lsp/markdown-renderer.ts +59 -0
  149. package/src/core/datasets/data-source-connections.ts +2 -0
  150. package/src/core/errors/errors.ts +1 -1
  151. package/src/core/storage/__tests__/state.test.ts +49 -0
  152. package/src/core/storage/__tests__/useStorageEntries.test.tsx +199 -3
  153. package/src/core/storage/state.ts +124 -13
  154. package/src/core/storage/types.ts +6 -0
  155. package/src/core/wasm/worker/bootstrap.ts +12 -4
  156. package/src/plugins/core/__test__/registerReactComponent.test.ts +96 -0
  157. package/src/plugins/core/registerReactComponent.tsx +42 -10
  158. package/src/plugins/impl/MultiselectPlugin.tsx +19 -142
  159. package/src/plugins/impl/SearchableSelect.tsx +16 -97
  160. package/src/plugins/impl/SliderPlugin.tsx +132 -18
  161. package/src/plugins/impl/__tests__/DropdownPlugin.test.tsx +5 -2
  162. package/src/plugins/impl/__tests__/MultiSelectPlugin.test.ts +1 -1
  163. package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +278 -8
  164. package/src/utils/__tests__/numbers.test.ts +20 -0
  165. package/src/utils/errors.ts +9 -0
  166. package/src/utils/lazy.ts +6 -1
  167. package/src/utils/numbers.ts +27 -0
  168. package/src/utils/schedule-task.ts +71 -0
  169. package/dist/assets/__vite-browser-external-Ci2ZQfXU.js +0 -1
  170. package/dist/assets/worker-ip3AI_sN.js +0 -73
  171. package/dist/dist-0Fif7jnk.js +0 -5
  172. package/dist/dist-B5h_9sHB.js +0 -6
  173. package/dist/dist-B9M6R5ye.js +0 -5
  174. package/dist/dist-BCt3tnck.js +0 -8
  175. package/dist/dist-BUIJwMwn.js +0 -8
  176. package/dist/dist-BpquMd3k.js +0 -5
  177. package/dist/dist-BzJsqYfz.js +0 -5
  178. package/dist/dist-CA5ELXAf.js +0 -6
  179. package/dist/dist-CLBRs6Uv.js +0 -5
  180. package/dist/dist-CStVCMbq.js +0 -5
  181. package/dist/dist-CZRIEY3Y.js +0 -8
  182. package/dist/dist-CuUHbFD0.js +0 -5
  183. package/dist/dist-DV7Iabxb.js +0 -8
  184. package/dist/dist-DhHh0jLg.js +0 -1247
  185. package/dist/dist-DuEeHMvL.js +0 -5
  186. package/dist/esm-CCuYCd3R.js +0 -1171
  187. package/src/plugins/impl/multiselectFilterFn.tsx +0 -22
  188. /package/src/components/{data-table → ui}/value-chips.tsx +0 -0
@@ -9,8 +9,6 @@ import {
9
9
  CopyMinusIcon,
10
10
  DownloadIcon,
11
11
  ExternalLinkIcon,
12
- EyeIcon,
13
- EyeOffIcon,
14
12
  FilePlus2Icon,
15
13
  FolderPlusIcon,
16
14
  ListTreeIcon,
@@ -43,6 +41,7 @@ import {
43
41
  MENU_ITEM_ICON_CLASS,
44
42
  RefreshIconButton,
45
43
  TreeChevron,
44
+ VisibilityToggleButton,
46
45
  } from "@/components/editor/file-tree/tree-actions";
47
46
  import { MarimoIcon, MarimoPlusIcon } from "@/components/icons/marimo-icons";
48
47
  import { Spinner } from "@/components/icons/spinner";
@@ -338,22 +337,13 @@ const Toolbar = ({
338
337
  data-testid="file-explorer-refresh-button"
339
338
  onClick={onRefresh}
340
339
  />
341
- <Tooltip
342
- content={showHiddenFiles ? "Hide hidden files" : "Show hidden files"}
343
- >
344
- <Button
345
- data-testid="file-explorer-hidden-files-button"
346
- onClick={onHidden}
347
- variant="text"
348
- size="xs"
349
- >
350
- {showHiddenFiles ? (
351
- <EyeIcon size={16} className="text-primary" />
352
- ) : (
353
- <EyeOffIcon size={16} />
354
- )}
355
- </Button>
356
- </Tooltip>
340
+ <VisibilityToggleButton
341
+ data-testid="file-explorer-hidden-files-button"
342
+ isVisible={showHiddenFiles}
343
+ onToggle={onHidden}
344
+ showTooltip="Show hidden files"
345
+ hideTooltip="Hide hidden files"
346
+ />
357
347
  <Tooltip content="Collapse all folders">
358
348
  <Button
359
349
  data-testid="file-explorer-collapse-button"
@@ -2,11 +2,13 @@
2
2
 
3
3
  import {
4
4
  ChevronRightIcon,
5
+ EyeIcon,
6
+ EyeOffIcon,
5
7
  MoreVerticalIcon,
6
8
  RefreshCwIcon,
7
9
  } from "lucide-react";
8
10
  import React, { useCallback, useState } from "react";
9
- import { Button } from "@/components/ui/button";
11
+ import { Button, type ButtonProps } from "@/components/ui/button";
10
12
  import { Tooltip } from "@/components/ui/tooltip";
11
13
  import { cn } from "@/utils/cn";
12
14
 
@@ -73,6 +75,49 @@ export const RefreshIconButton: React.FC<{
73
75
  return <Tooltip content={tooltip}>{button}</Tooltip>;
74
76
  };
75
77
 
78
+ /**
79
+ * Toggle button that switches between an eye (visible) and crossed-out eye
80
+ * (hidden) icon. Used to show/hide optional items in toolbars, e.g. hidden
81
+ * files in the file explorer or empty schemas in the data sources panel.
82
+ */
83
+ export const VisibilityToggleButton: React.FC<{
84
+ /** Whether the optional items are currently visible. */
85
+ isVisible: boolean;
86
+ onToggle: () => void;
87
+ showTooltip: string;
88
+ hideTooltip: string;
89
+ size?: ButtonProps["size"];
90
+ className?: string;
91
+ iconClassName?: string;
92
+ "data-testid"?: string;
93
+ }> = ({
94
+ isVisible,
95
+ onToggle,
96
+ showTooltip,
97
+ hideTooltip,
98
+ size = "xs",
99
+ className,
100
+ iconClassName,
101
+ "data-testid": dataTestId,
102
+ }) => {
103
+ const Icon = isVisible ? EyeIcon : EyeOffIcon;
104
+ return (
105
+ <Tooltip content={isVisible ? hideTooltip : showTooltip}>
106
+ <Button
107
+ data-testid={dataTestId}
108
+ variant="text"
109
+ size={size}
110
+ className={className}
111
+ onClick={onToggle}
112
+ >
113
+ <Icon
114
+ className={cn("h-4 w-4", isVisible && "text-primary", iconClassName)}
115
+ />
116
+ </Button>
117
+ </Tooltip>
118
+ );
119
+ };
120
+
76
121
  /**
77
122
  * Three-dot menu trigger that fades in on row hover.
78
123
  * Must be inside a `group` container.
@@ -58,7 +58,6 @@ import type { UserConfig } from "../../core/config/config-schema";
58
58
  import { isAppInteractionDisabled } from "../../core/websocket/connection-utils";
59
59
  import { useCellRenderCount } from "../../hooks/useCellRenderCount";
60
60
  import type { Theme } from "../../theme/useTheme";
61
- import { derefNotNull } from "../../utils/dereference";
62
61
  import { Functions } from "../../utils/functions";
63
62
  import { Logger } from "../../utils/Logger";
64
63
  import { renderShortcut } from "../shortcuts/renderShortcut";
@@ -233,8 +232,9 @@ export type CellComponentActions = Pick<
233
232
  export interface CellHandle {
234
233
  /**
235
234
  * The CodeMirror editor view.
235
+ * TODO: merge this with editorViewOrNull
236
236
  */
237
- editorView: EditorView;
237
+ editorView: EditorView | null;
238
238
  /**
239
239
  * The CodeMirror editor view, or null if it is not yet mounted.
240
240
  */
@@ -280,7 +280,7 @@ const CellComponent = (props: CellProps) => {
280
280
  ref,
281
281
  () => ({
282
282
  get editorView() {
283
- return derefNotNull(editorView);
283
+ return editorView.current;
284
284
  },
285
285
  get editorViewOrNull() {
286
286
  return editorView.current;
@@ -248,6 +248,26 @@ const BACKWARDS_COMPAT_SNAPSHOTS: BackwardsCompatCase[] = [
248
248
  ],
249
249
  },
250
250
  },
251
+ {
252
+ // `showCode` was added to SlideConfig. The validator must know about it (so
253
+ // it isn't silently stripped), the deserializer must carry it through, and
254
+ // serialize → deserialize must round-trip both values.
255
+ label: "showCode round-trips through validate + (de)serialize",
256
+ input: {
257
+ cells: [
258
+ { type: "slide", showCode: true },
259
+ { type: "fragment", showCode: false },
260
+ ],
261
+ },
262
+ expected: {
263
+ deck: {},
264
+ cellIds: ["a", "b"],
265
+ cellEntries: [
266
+ ["a", { type: "slide", showCode: true }],
267
+ ["b", { type: "fragment", showCode: false }],
268
+ ],
269
+ },
270
+ },
251
271
  ];
252
272
 
253
273
  describe("SlidesLayoutPlugin backwards compatibility", () => {
@@ -9,6 +9,7 @@ export type SlideType = z.infer<typeof SlideTypeSchema>;
9
9
  const SlideConfigSchema = z.looseObject({
10
10
  type: SlideTypeSchema.optional(),
11
11
  speakerNotes: z.string().optional(),
12
+ showCode: z.boolean().optional(),
12
13
  });
13
14
  export type SlideConfig = z.infer<typeof SlideConfigSchema>;
14
15
 
@@ -0,0 +1,43 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { render, screen, waitFor } from "@testing-library/react";
3
+ import { describe, expect, it } from "vitest";
4
+ import { MarkdownRenderer } from "../markdown-renderer";
5
+
6
+ describe("MarkdownRenderer", () => {
7
+ // Regression test for https://github.com/marimo-team/marimo/issues/9847
8
+ it("preserves class names on raw HTML (e.g. marimo admonitions)", async () => {
9
+ const { container } = render(
10
+ <MarkdownRenderer
11
+ content={
12
+ '<div class="admonition error">' +
13
+ '<p class="admonition-title">Error</p>' +
14
+ "<p>Nope!</p>" +
15
+ "</div>"
16
+ }
17
+ />,
18
+ );
19
+
20
+ await waitFor(() => {
21
+ expect(screen.getByText("Nope!")).toBeInTheDocument();
22
+ });
23
+
24
+ const admonition = container.querySelector(".admonition.error");
25
+ expect(admonition).toBeInTheDocument();
26
+ expect(container.querySelector(".admonition-title")).toBeInTheDocument();
27
+ });
28
+
29
+ it("strips unsafe tags while keeping class names", async () => {
30
+ const { container } = render(
31
+ <MarkdownRenderer
32
+ content={'<div class="safe"><script>alert(1)</script>hello</div>'}
33
+ />,
34
+ );
35
+
36
+ await waitFor(() => {
37
+ expect(screen.getByText("hello")).toBeInTheDocument();
38
+ });
39
+
40
+ expect(container.querySelector(".safe")).toBeInTheDocument();
41
+ expect(container.querySelector("script")).not.toBeInTheDocument();
42
+ });
43
+ });
@@ -4,7 +4,12 @@ import { EditorView } from "@codemirror/view";
4
4
  import { useAtomValue } from "jotai";
5
5
  import { BetweenHorizontalStartIcon } from "lucide-react";
6
6
  import { memo, Suspense, useState } from "react";
7
- import { Streamdown, type StreamdownProps } from "streamdown";
7
+ import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
8
+ import {
9
+ defaultRehypePlugins,
10
+ Streamdown,
11
+ type StreamdownProps,
12
+ } from "streamdown";
8
13
  import { Button, type ButtonProps } from "@/components/ui/button";
9
14
  import { maybeAddMarimoImport } from "@/core/cells/add-missing-import";
10
15
  import { useCellActions } from "@/core/cells/cells";
@@ -148,6 +153,23 @@ const CopyButton: React.FC<ButtonProps> = ({ onClick, ...props }) => {
148
153
  );
149
154
  };
150
155
 
156
+ // Allow `className` on every element; the default GitHub schema strips it,
157
+ // which drops the styling on marimo HTML like `mo.md(...)` admonitions.
158
+ const sanitizeSchema = {
159
+ ...defaultSchema,
160
+ attributes: {
161
+ ...defaultSchema.attributes,
162
+ "*": [...(defaultSchema.attributes?.["*"] ?? []), "className"],
163
+ },
164
+ };
165
+
166
+ // Keep Streamdown's other rehype plugins (raw, harden) so scripts and unsafe
167
+ // URLs are still sanitized.
168
+ const REHYPE_PLUGINS: StreamdownProps["rehypePlugins"] = Object.values({
169
+ ...defaultRehypePlugins,
170
+ sanitize: [rehypeSanitize, sanitizeSchema],
171
+ });
172
+
151
173
  type Components = StreamdownProps["components"];
152
174
 
153
175
  const COMPONENTS: Components = {
@@ -187,6 +209,7 @@ export const MarkdownRenderer = memo(({ content }: { content: string }) => {
187
209
  <Streamdown
188
210
  components={COMPONENTS}
189
211
  plugins={plugins}
212
+ rehypePlugins={REHYPE_PLUGINS}
190
213
  className="mo-markdown-renderer"
191
214
  >
192
215
  {content}
@@ -0,0 +1,166 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { fireEvent, render, screen } from "@testing-library/react";
4
+ import { beforeAll, describe, expect, it, vi } from "vitest";
5
+ import { cellId } from "@/__tests__/branded";
6
+ import { TooltipProvider } from "@/components/ui/tooltip";
7
+ import type { CellId } from "@/core/cells/ids";
8
+ import type { CellData, CellRuntimeState } from "@/core/cells/types";
9
+ import { MultiColumn } from "@/utils/id-tree";
10
+ import { SlidesMinimap } from "../minimap";
11
+
12
+ const A = cellId("a");
13
+ const B = cellId("b");
14
+
15
+ // Spies shared with the hoisted module mocks below.
16
+ const { createNewCell, deleteCell, moveCellToIndex } = vi.hoisted(() => ({
17
+ createNewCell: vi.fn(),
18
+ deleteCell: vi.fn(),
19
+ moveCellToIndex: vi.fn(),
20
+ }));
21
+
22
+ vi.mock("@/core/cells/cells", async (importOriginal) => {
23
+ const actual = await importOriginal<typeof import("@/core/cells/cells")>();
24
+ return {
25
+ ...actual,
26
+ useCellActions: () => ({ moveCellToIndex, createNewCell }),
27
+ useCellIds: () => MultiColumn.from([[A, B]]),
28
+ };
29
+ });
30
+
31
+ vi.mock("@/components/editor/cell/useDeleteCell", () => ({
32
+ useDeleteCellCallback: () => deleteCell,
33
+ }));
34
+
35
+ beforeAll(() => {
36
+ // jsdom doesn't implement these; radix menus poke at them.
37
+ global.HTMLElement.prototype.scrollIntoView = () => {
38
+ /* noop */
39
+ };
40
+ if (!global.HTMLElement.prototype.hasPointerCapture) {
41
+ global.HTMLElement.prototype.hasPointerCapture = () => false;
42
+ }
43
+ if (!global.HTMLElement.prototype.releasePointerCapture) {
44
+ global.HTMLElement.prototype.releasePointerCapture = () => {
45
+ /* noop */
46
+ };
47
+ }
48
+ global.IntersectionObserver ??= class {
49
+ observe() {
50
+ /* noop */
51
+ }
52
+ unobserve() {
53
+ /* noop */
54
+ }
55
+ disconnect() {
56
+ /* noop */
57
+ }
58
+ takeRecords() {
59
+ return [];
60
+ }
61
+ root = null;
62
+ rootMargin = "";
63
+ thresholds = [];
64
+ } as unknown as typeof IntersectionObserver;
65
+ });
66
+
67
+ // The minimap only reads `id`/`code`/`status`/`output`, so a minimal stub is
68
+ // enough; the cast is confined to this helper.
69
+ function makeCell(id: CellId): CellRuntimeState & CellData {
70
+ return {
71
+ id,
72
+ code: `print("${id}")`,
73
+ output: null,
74
+ status: "idle",
75
+ } as unknown as CellRuntimeState & CellData;
76
+ }
77
+
78
+ function renderMinimap() {
79
+ const onSlideClick = vi.fn();
80
+ const utils = render(
81
+ <TooltipProvider>
82
+ <SlidesMinimap
83
+ cells={[makeCell(A), makeCell(B)]}
84
+ thumbnailWidth={200}
85
+ canReorder={false}
86
+ activeCellId={null}
87
+ onSlideClick={onSlideClick}
88
+ />
89
+ </TooltipProvider>,
90
+ );
91
+ return { ...utils, onSlideClick };
92
+ }
93
+
94
+ const EMPTY_CELL = { code: "", autoFocus: false } as const;
95
+
96
+ describe("SlidesMinimap insert lines", () => {
97
+ it("inserts a blank cell above the first row and below any row", () => {
98
+ renderMinimap();
99
+ // First row exposes both an above and a below line; later rows only below.
100
+ // DOM order: [A-above, A-below, B-below].
101
+ const inserts = screen.getAllByTestId("minimap-insert-cell");
102
+ expect(inserts).toHaveLength(3);
103
+
104
+ fireEvent.click(inserts[0]);
105
+ expect(createNewCell).toHaveBeenLastCalledWith({
106
+ cellId: A,
107
+ before: true,
108
+ ...EMPTY_CELL,
109
+ });
110
+
111
+ fireEvent.click(inserts[1]);
112
+ expect(createNewCell).toHaveBeenLastCalledWith({
113
+ cellId: A,
114
+ before: false,
115
+ ...EMPTY_CELL,
116
+ });
117
+
118
+ fireEvent.click(inserts[2]);
119
+ expect(createNewCell).toHaveBeenLastCalledWith({
120
+ cellId: B,
121
+ before: false,
122
+ ...EMPTY_CELL,
123
+ });
124
+ });
125
+ });
126
+
127
+ describe("SlidesMinimap context menu", () => {
128
+ const openRowMenu = (container: HTMLElement, id: CellId) => {
129
+ const row = container.querySelector<HTMLElement>(`[data-cell-id="${id}"]`);
130
+ expect(row).not.toBeNull();
131
+ fireEvent.contextMenu(row!);
132
+ };
133
+
134
+ it('"Add cell" inserts a blank cell below the row', () => {
135
+ const { container } = renderMinimap();
136
+ openRowMenu(container, A);
137
+ fireEvent.click(screen.getByText("Add cell"));
138
+ expect(createNewCell).toHaveBeenCalledWith({
139
+ cellId: A,
140
+ before: false,
141
+ ...EMPTY_CELL,
142
+ });
143
+ });
144
+
145
+ it('"Delete cell" deletes the row\'s cell', () => {
146
+ const { container } = renderMinimap();
147
+ openRowMenu(container, B);
148
+ fireEvent.click(screen.getByText("Delete cell"));
149
+ expect(deleteCell).toHaveBeenCalledWith({ cellId: B });
150
+ });
151
+ });
152
+
153
+ describe("SlidesMinimap keyboard activation", () => {
154
+ it("activates the row on Enter but ignores Space (reserved by reveal.js)", () => {
155
+ const { container, onSlideClick } = renderMinimap();
156
+ const row = container.querySelector<HTMLElement>(`[data-cell-id="${A}"]`);
157
+ expect(row).not.toBeNull();
158
+
159
+ fireEvent.keyDown(row!, { key: "Enter" });
160
+ expect(onSlideClick).toHaveBeenCalledWith(0);
161
+
162
+ onSlideClick.mockClear();
163
+ fireEvent.keyDown(row!, { key: " " });
164
+ expect(onSlideClick).not.toHaveBeenCalled();
165
+ });
166
+ });