@marimo-team/islands 0.23.10-dev3 → 0.23.10-dev30
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.
- package/dist/{ConnectedDataExplorerComponent-CyV83R2m.js → ConnectedDataExplorerComponent-DmBropAy.js} +31 -31
- package/dist/{ErrorBoundary-rULOrC_p.js → ErrorBoundary-DpbaKVv7.js} +1 -1
- package/dist/{any-language-editor-DfdpyDv_.js → any-language-editor-DNmoSiWL.js} +20 -20
- package/dist/assets/__vite-browser-external-eshhtsgZ.js +1 -0
- package/dist/assets/worker-CC0Oul9k.js +73 -0
- package/dist/{chat-ui-C1tL1pML.js → chat-ui-D6oraHT2.js} +76 -76
- package/dist/{check-DTbrK0zt.js → check-BCaJeT-J.js} +1 -1
- package/dist/{code-visibility-DfnO0DcH.js → code-visibility-wR7WSQ4c.js} +2166 -1292
- package/dist/{copy-BuQpJEzp.js → copy-UqRYxiOg.js} +33 -33
- package/dist/dist-7QfXoMdB.js +5 -0
- package/dist/{dist-DgnE8F-r.js → dist-A2846XWO.js} +1 -1
- package/dist/dist-BEXXyZig.js +5 -0
- package/dist/{dist-B3pZ0Ab6.js → dist-BR_gyG9L.js} +3 -3
- package/dist/{dist-CcXxepx6.js → dist-BSAt6RhH.js} +27 -27
- package/dist/{dist-Bde4a2kU.js → dist-BY018Paw.js} +8 -8
- package/dist/dist-BYj57OV4.js +5 -0
- package/dist/{dist-CUCNs1ja.js → dist-BaoDKvdy.js} +2 -2
- package/dist/{dist-Cy1WxgBD.js → dist-Bf7SHuNp.js} +5 -5
- package/dist/{dist-Bz_sYWbr.js → dist-Bk75fBZA.js} +2 -2
- package/dist/dist-BlSvQzNr.js +5 -0
- package/dist/{dist-C5VC_yzu.js → dist-BzEzfugY.js} +1 -1
- package/dist/dist-CCBlxAgS.js +8 -0
- package/dist/dist-CIDTVIUf.js +5 -0
- package/dist/{dist-CLUtPrdy.js → dist-CIYBwstr.js} +1 -1
- package/dist/{dist-BotSqB48.js → dist-C_Y3oV3C.js} +12 -12
- package/dist/{dist-BTfv03uy.js → dist-CcWX6tmx.js} +2 -2
- package/dist/{dist-BhM8gdSO.js → dist-CoXAujgg.js} +4 -4
- package/dist/{dist-4j4c7bjm.js → dist-CpxNdDkw.js} +3 -3
- package/dist/dist-CqQyhAM8.js +8 -0
- package/dist/dist-CwRu2Xzh.js +5 -0
- package/dist/{dist-BcuoonNH.js → dist-CxJDU6Bh.js} +9 -9
- package/dist/{dist-DxvORzUR.js → dist-D-W5ny5a.js} +8 -8
- package/dist/dist-D8CDTVgf.js +6 -0
- package/dist/dist-D8DNB0nO.js +8 -0
- package/dist/dist-DL6N_q-A.js +5 -0
- package/dist/{dist-BbbIBDiQ.js → dist-DMjWuVs8.js} +1 -1
- package/dist/dist-DOFbNV_b.js +8 -0
- package/dist/dist-DPrYzMY0.js +6 -0
- package/dist/{dist-h2c8sZvT.js → dist-DZORgqKY.js} +1 -1
- package/dist/{dist-B3P2fFpz.js → dist-DZo4nSS0.js} +14 -14
- package/dist/{dist-D4CewLk6.js → dist-Dax--nl9.js} +1 -1
- package/dist/{dist-DRfcqpxJ.js → dist-DgGbNavJ.js} +2 -2
- package/dist/{dist-C1BYNeCR.js → dist-Dk6PV_d3.js} +10 -10
- package/dist/{dist-fQ0ViXGs.js → dist-Dv_Y15yk.js} +107 -107
- package/dist/{dist-Bfwsv11D.js → dist-DyyjKEYf.js} +2 -2
- package/dist/{dist-p2qyWijU.js → dist-GZXUmt0b.js} +2 -2
- package/dist/{dist-CLJWPTX2.js → dist-LTU8Hdvn.js} +3 -3
- package/dist/{dist-DqAWR3CS.js → dist-M9Vag9Y0.js} +20 -20
- package/dist/{dist-DNdhYsgW.js → dist-U4F-tbMs.js} +79 -62
- package/dist/{dist-RqXTaiir.js → dist-abid3KgM.js} +11 -11
- package/dist/dist-cdmMjgsn.js +5 -0
- package/dist/dist-hT4QzYX-.js +1247 -0
- package/dist/{dist-luvabDEB.js → dist-t9Kf7xqC.js} +2 -2
- package/dist/{error-banner-5bz0L9hS.js → error-banner-Cc0I3C9e.js} +1 -1
- package/dist/esm-BaH2eg5-.js +1171 -0
- package/dist/{esm-Duie8iU-.js → esm-ga2Bf3O2.js} +43 -43
- package/dist/{extends-BgdxCfYu.js → extends-D_hDsj6R.js} +4 -4
- package/dist/{formats-DHxc-FdY.js → formats-C4wO47tk.js} +1 -1
- package/dist/{glide-data-editor-BOmK9ETQ.js → glide-data-editor-Qhu8oCX-.js} +12 -12
- package/dist/{html-to-image-CNa5ok96.js → html-to-image-UEH5lFDZ.js} +2318 -2275
- package/dist/{input-_2sjvfne.js → input-CMYy4hzj.js} +187 -185
- package/dist/{label-LWtdw5i8.js → label-CC4ytI1X.js} +1 -1
- package/dist/main.js +6941 -6913
- package/dist/{mermaid-lXOw5Py9.js → mermaid-zuLgJ8J8.js} +4 -4
- package/dist/{process-output-DKr4f1di.js → process-output-CyMLTogj.js} +3 -3
- package/dist/{reveal-component-UdMnCK5U.js → reveal-component-BjnkUAZ9.js} +697 -619
- package/dist/{spec-B96zNUEA.js → spec-X7FwLJni.js} +4 -4
- package/dist/{strings-Bu3vlb6W.js → strings-J57tzLr3.js} +47 -46
- package/dist/style.css +1 -1
- package/dist/{toDate-x-WRDCH7.js → toDate-d8RCRrRd.js} +2 -2
- package/dist/{tooltip-C5FYOpQc.js → tooltip-DpcyNkQ2.js} +2 -2
- package/dist/{types-CVvp1fKr.js → types-ChtMFmZ2.js} +1 -1
- package/dist/{useAsyncData-iRgKDT5s.js → useAsyncData-PonK__yh.js} +1 -1
- package/dist/{useDateFormatter-BA4FCquG.js → useDateFormatter-QB-3MpYr.js} +2 -2
- package/dist/{useDeepCompareMemoize-CkQ57VS2.js → useDeepCompareMemoize-D3NGWke6.js} +1 -1
- package/dist/{useLifecycle-BBO9PIph.js → useLifecycle-00mO3OSS.js} +2 -2
- package/dist/{useTheme-DHIrRQOe.js → useTheme-DEhDzATN.js} +1 -1
- package/dist/{vega-component-Dq-SH463.js → vega-component-9h1ACS78.js} +8 -8
- package/dist/{zod-CoBiJ5v4.js → zod-aLSua2NL.js} +24 -23
- package/package.json +3 -3
- package/src/components/data-table/TableBottomBar.tsx +1 -15
- package/src/components/data-table/TableTopBar.tsx +8 -13
- package/src/components/data-table/__tests__/TableBottomBar.test.tsx +6 -12
- package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +227 -0
- package/src/components/data-table/__tests__/data-table.test.tsx +154 -12
- package/src/components/data-table/column-visibility-dropdown.tsx +204 -0
- package/src/components/data-table/data-table.tsx +1 -1
- package/src/components/data-table/filter-by-values-picker.tsx +39 -17
- package/src/components/data-table/filter-pills.tsx +1 -1
- package/src/components/data-table/hover-tooltip/__tests__/content.test.ts +60 -0
- package/src/components/data-table/hover-tooltip/content.ts +44 -0
- package/src/components/data-table/hover-tooltip/hover-tooltip.tsx +55 -0
- package/src/components/data-table/hover-tooltip/use-table-hover-tooltip.ts +159 -0
- package/src/components/data-table/renderers.tsx +27 -43
- package/src/components/datasources/__tests__/filter-empty.test.ts +183 -0
- package/src/components/datasources/datasources.tsx +92 -3
- package/src/components/editor/cell/cell-context-menu.tsx +15 -2
- package/src/components/editor/cell/code/language-toggle.tsx +7 -1
- package/src/components/editor/chrome/wrapper/app-chrome.tsx +97 -52
- package/src/components/editor/chrome/wrapper/lazy-panels.ts +91 -0
- package/src/components/editor/chrome/wrapper/sidebar.tsx +2 -0
- package/src/components/editor/documentation.css +35 -0
- package/src/components/editor/file-tree/file-explorer.tsx +8 -18
- package/src/components/editor/file-tree/tree-actions.tsx +46 -1
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +20 -0
- package/src/components/editor/renderers/slides-layout/types.ts +1 -0
- package/src/components/slides/__tests__/minimap-actions.test.tsx +166 -0
- package/src/components/slides/__tests__/reveal-component.test.ts +425 -0
- package/src/components/slides/minimap.tsx +127 -10
- package/src/components/slides/reveal-component.tsx +287 -61
- package/src/components/slides/slide-cell-view.tsx +26 -2
- package/src/components/slides/slide-form.tsx +26 -4
- package/src/components/storage/__tests__/storage-inspector.test.ts +53 -0
- package/src/components/storage/storage-inspector.tsx +68 -48
- package/src/components/ui/__tests__/use-toast.test.ts +75 -0
- package/src/components/ui/combobox.tsx +51 -32
- package/src/components/ui/reorderable-list.tsx +13 -0
- package/src/components/ui/select-core/__tests__/use-select-list.test.ts +294 -0
- package/src/components/ui/select-core/__tests__/utils.test.ts +222 -0
- package/src/components/ui/select-core/index.ts +16 -0
- package/src/components/ui/select-core/option-row.tsx +33 -0
- package/src/components/ui/select-core/render-slot.ts +20 -0
- package/src/components/ui/select-core/select-list.tsx +248 -0
- package/src/components/ui/select-core/types.ts +44 -0
- package/src/components/ui/select-core/use-select-list.ts +347 -0
- package/src/components/ui/select-core/utils.ts +121 -0
- package/src/components/ui/use-toast.ts +33 -13
- package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +0 -28
- package/src/core/cells/__tests__/cell.test.ts +29 -2
- package/src/core/cells/cell.ts +5 -1
- package/src/core/codemirror/go-to-definition/commands.ts +4 -3
- package/src/core/codemirror/language/languages/python.ts +2 -0
- package/src/core/codemirror/language/languages/sql/utils.ts +3 -1
- package/src/core/codemirror/lsp/__tests__/markdown-renderer.test.ts +41 -0
- package/src/core/codemirror/lsp/markdown-renderer.ts +59 -0
- package/src/core/datasets/data-source-connections.ts +2 -0
- package/src/core/network/__tests__/requests-static.test.ts +30 -0
- package/src/core/network/requests-static.ts +14 -10
- package/src/core/wasm/worker/bootstrap.ts +12 -4
- package/src/plugins/impl/MultiselectPlugin.tsx +19 -142
- package/src/plugins/impl/SearchableSelect.tsx +16 -97
- package/src/plugins/impl/__tests__/DropdownPlugin.test.tsx +5 -2
- package/src/plugins/impl/__tests__/MultiSelectPlugin.test.ts +1 -1
- package/src/plugins/layout/DownloadPlugin.tsx +1 -1
- package/src/utils/lazy.ts +6 -1
- package/dist/assets/__vite-browser-external-Ci2ZQfXU.js +0 -1
- package/dist/assets/worker-ip3AI_sN.js +0 -73
- package/dist/dist-0Fif7jnk.js +0 -5
- package/dist/dist-B5h_9sHB.js +0 -6
- package/dist/dist-B9M6R5ye.js +0 -5
- package/dist/dist-BCt3tnck.js +0 -8
- package/dist/dist-BUIJwMwn.js +0 -8
- package/dist/dist-BpquMd3k.js +0 -5
- package/dist/dist-BzJsqYfz.js +0 -5
- package/dist/dist-CA5ELXAf.js +0 -6
- package/dist/dist-CLBRs6Uv.js +0 -5
- package/dist/dist-CStVCMbq.js +0 -5
- package/dist/dist-CZRIEY3Y.js +0 -8
- package/dist/dist-CuUHbFD0.js +0 -5
- package/dist/dist-DV7Iabxb.js +0 -8
- package/dist/dist-DhHh0jLg.js +0 -1247
- package/dist/dist-DuEeHMvL.js +0 -5
- package/dist/esm-BfhQmZjp.js +0 -1171
- package/src/plugins/impl/multiselectFilterFn.tsx +0 -22
- /package/src/components/{data-table → ui}/value-chips.tsx +0 -0
|
@@ -34,6 +34,26 @@ import { ErrorBoundary } from "../../boundary/ErrorBoundary";
|
|
|
34
34
|
import { raf2 } from "../../navigation/focus-utils";
|
|
35
35
|
import { ContextAwarePanel } from "../panels/context-aware-panel/context-aware-panel";
|
|
36
36
|
import { PanelSectionProvider } from "../panels/panel-context";
|
|
37
|
+
import { useTheme } from "@/theme/useTheme";
|
|
38
|
+
import {
|
|
39
|
+
LazyAgentPanel,
|
|
40
|
+
LazyCachePanel,
|
|
41
|
+
LazyChatPanel,
|
|
42
|
+
LazyDependencyGraphPanel,
|
|
43
|
+
LazyDocumentationPanel,
|
|
44
|
+
LazyErrorsPanel,
|
|
45
|
+
LazyFileExplorerPanel,
|
|
46
|
+
LazyLogsPanel,
|
|
47
|
+
LazyOutlinePanel,
|
|
48
|
+
LazyPackagesPanel,
|
|
49
|
+
LazyScratchpadPanel,
|
|
50
|
+
LazySecretsPanel,
|
|
51
|
+
LazySessionPanel,
|
|
52
|
+
LazySnippetsPanel,
|
|
53
|
+
LazyTerminal,
|
|
54
|
+
LazyTracingPanel,
|
|
55
|
+
PANEL_PRELOADERS,
|
|
56
|
+
} from "./lazy-panels";
|
|
37
57
|
import { panelLayoutAtom, useChromeActions, useChromeState } from "../state";
|
|
38
58
|
import {
|
|
39
59
|
isPanelHidden,
|
|
@@ -50,33 +70,26 @@ import { useAiPanelTab } from "./useAiPanel";
|
|
|
50
70
|
import { useDependencyPanelTab } from "./useDependencyPanelTab";
|
|
51
71
|
import { handleDragging } from "./utils";
|
|
52
72
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
() => import("../panels/scratchpad-panel"),
|
|
74
|
-
);
|
|
75
|
-
const LazySecretsPanel = React.lazy(() => import("../panels/secrets-panel"));
|
|
76
|
-
const LazySnippetsPanel = React.lazy(() => import("../panels/snippets-panel"));
|
|
77
|
-
const LazyTracingPanel = React.lazy(() => import("../panels/tracing-panel"));
|
|
78
|
-
const LazyCachePanel = React.lazy(() => import("../panels/cache-panel"));
|
|
79
|
-
|
|
73
|
+
// Placeholder that matches the eventual xterm theme background so the
|
|
74
|
+
// transition into the loaded terminal is seamless rather than a blank flash.
|
|
75
|
+
const TerminalSkeleton: React.FC = () => {
|
|
76
|
+
const { theme } = useTheme();
|
|
77
|
+
const isDark = theme === "dark";
|
|
78
|
+
return (
|
|
79
|
+
<div
|
|
80
|
+
aria-label="Loading terminal"
|
|
81
|
+
role="status"
|
|
82
|
+
className="w-full h-full flex items-start p-3 font-mono text-xs select-none"
|
|
83
|
+
style={{
|
|
84
|
+
background: isDark ? "#0f172a" : "#ffffff",
|
|
85
|
+
color: isDark ? "#94a3b8" : "#64748b",
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
<span className="opacity-70">Starting terminal</span>
|
|
89
|
+
<span className="ml-1 inline-block w-2 h-3.5 align-middle bg-current animate-pulse" />
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
};
|
|
80
93
|
export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
|
|
81
94
|
const {
|
|
82
95
|
isSidebarOpen,
|
|
@@ -96,6 +109,33 @@ export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
|
|
|
96
109
|
const capabilities = useAtomValue(capabilitiesAtom);
|
|
97
110
|
const aiEnabled = useAtomValue(aiEnabledAtom);
|
|
98
111
|
|
|
112
|
+
// On mount, idle-preload whichever panels the user had open at last unload,
|
|
113
|
+
// so the first interaction with the sidebar/dev panel doesn't hit a cold
|
|
114
|
+
// chunk fetch. Runs once.
|
|
115
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
const preloadOpenPanels = () => {
|
|
118
|
+
if (isSidebarOpen && selectedPanel) {
|
|
119
|
+
PANEL_PRELOADERS[selectedPanel]?.();
|
|
120
|
+
}
|
|
121
|
+
if (isDeveloperPanelOpen && selectedDeveloperPanelTab) {
|
|
122
|
+
PANEL_PRELOADERS[selectedDeveloperPanelTab]?.();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const canIdle =
|
|
127
|
+
typeof window !== "undefined" &&
|
|
128
|
+
typeof window.requestIdleCallback === "function";
|
|
129
|
+
if (canIdle) {
|
|
130
|
+
const handle = window.requestIdleCallback(preloadOpenPanels, {
|
|
131
|
+
timeout: 2000,
|
|
132
|
+
});
|
|
133
|
+
return () => window.cancelIdleCallback(handle);
|
|
134
|
+
}
|
|
135
|
+
const handle = setTimeout(preloadOpenPanels, 300);
|
|
136
|
+
return () => clearTimeout(handle);
|
|
137
|
+
}, []);
|
|
138
|
+
|
|
99
139
|
// Convert current developer panel items to PanelDescriptors
|
|
100
140
|
// Filter out hidden panels (e.g., terminal when capability is not available)
|
|
101
141
|
const devPanelItems = useMemo(() => {
|
|
@@ -256,32 +296,34 @@ export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
|
|
|
256
296
|
|
|
257
297
|
const renderAiPanel = () => {
|
|
258
298
|
if (agentsEnabled && aiPanelTab === "agents") {
|
|
259
|
-
return <LazyAgentPanel />;
|
|
299
|
+
return <LazyAgentPanel.Component />;
|
|
260
300
|
}
|
|
261
|
-
return <LazyChatPanel />;
|
|
301
|
+
return <LazyChatPanel.Component />;
|
|
262
302
|
};
|
|
263
303
|
|
|
264
304
|
const SIDEBAR_PANELS: Record<PanelType, React.ReactNode> = {
|
|
265
|
-
files: <LazyFileExplorerPanel />,
|
|
266
|
-
variables: <LazySessionPanel />,
|
|
267
|
-
dependencies: <LazyDependencyGraphPanel />,
|
|
268
|
-
packages: <LazyPackagesPanel />,
|
|
269
|
-
outline: <LazyOutlinePanel />,
|
|
270
|
-
documentation: <LazyDocumentationPanel />,
|
|
271
|
-
snippets: <LazySnippetsPanel />,
|
|
305
|
+
files: <LazyFileExplorerPanel.Component />,
|
|
306
|
+
variables: <LazySessionPanel.Component />,
|
|
307
|
+
dependencies: <LazyDependencyGraphPanel.Component />,
|
|
308
|
+
packages: <LazyPackagesPanel.Component />,
|
|
309
|
+
outline: <LazyOutlinePanel.Component />,
|
|
310
|
+
documentation: <LazyDocumentationPanel.Component />,
|
|
311
|
+
snippets: <LazySnippetsPanel.Component />,
|
|
272
312
|
ai: renderAiPanel(),
|
|
273
|
-
errors: <LazyErrorsPanel />,
|
|
274
|
-
scratchpad: <LazyScratchpadPanel />,
|
|
275
|
-
tracing: <LazyTracingPanel />,
|
|
276
|
-
secrets: <LazySecretsPanel />,
|
|
277
|
-
logs: <LazyLogsPanel />,
|
|
313
|
+
errors: <LazyErrorsPanel.Component />,
|
|
314
|
+
scratchpad: <LazyScratchpadPanel.Component />,
|
|
315
|
+
tracing: <LazyTracingPanel.Component />,
|
|
316
|
+
secrets: <LazySecretsPanel.Component />,
|
|
317
|
+
logs: <LazyLogsPanel.Component />,
|
|
278
318
|
terminal: (
|
|
279
|
-
<
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
319
|
+
<Suspense fallback={<TerminalSkeleton />}>
|
|
320
|
+
<LazyTerminal.Component
|
|
321
|
+
visible={isSidebarOpen && selectedPanel === "terminal"}
|
|
322
|
+
onClose={() => setIsSidebarOpen(false)}
|
|
323
|
+
/>
|
|
324
|
+
</Suspense>
|
|
283
325
|
),
|
|
284
|
-
cache: <LazyCachePanel />,
|
|
326
|
+
cache: <LazyCachePanel.Component />,
|
|
285
327
|
};
|
|
286
328
|
|
|
287
329
|
const helpPaneBody = (
|
|
@@ -414,12 +456,14 @@ export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
|
|
|
414
456
|
const DEVELOPER_PANELS: Record<PanelType, React.ReactNode> = {
|
|
415
457
|
...SIDEBAR_PANELS,
|
|
416
458
|
terminal: (
|
|
417
|
-
<
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
459
|
+
<Suspense fallback={<TerminalSkeleton />}>
|
|
460
|
+
<LazyTerminal.Component
|
|
461
|
+
visible={
|
|
462
|
+
isDeveloperPanelOpen && selectedDeveloperPanelTab === "terminal"
|
|
463
|
+
}
|
|
464
|
+
onClose={() => setIsDeveloperPanelOpen(false)}
|
|
465
|
+
/>
|
|
466
|
+
</Suspense>
|
|
423
467
|
),
|
|
424
468
|
};
|
|
425
469
|
|
|
@@ -474,6 +518,7 @@ export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
|
|
|
474
518
|
className="flex flex-row gap-1"
|
|
475
519
|
minItems={0}
|
|
476
520
|
onAction={(panel) => openApplication(panel.type)}
|
|
521
|
+
onItemPreloadHint={(panel) => PANEL_PRELOADERS[panel.type]?.()}
|
|
477
522
|
renderItem={(panel) => (
|
|
478
523
|
<div
|
|
479
524
|
className={cn(
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { reactLazyWithPreload } from "@/utils/lazy";
|
|
4
|
+
import { Logger } from "@/utils/Logger";
|
|
5
|
+
import type { PanelType } from "../types";
|
|
6
|
+
|
|
7
|
+
// Preloading is best-effort: a chunk-load failure here should not surface as
|
|
8
|
+
// an unhandled rejection (the panel will retry the import when actually
|
|
9
|
+
// rendered, where Suspense/ErrorBoundary handle the failure).
|
|
10
|
+
const safePreload = (lazy: { preload: () => Promise<unknown> }) => (): void => {
|
|
11
|
+
void lazy.preload().catch((error) => {
|
|
12
|
+
Logger.debug("Failed to preload panel chunk", error);
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Centralized lazy panels. Using reactLazyWithPreload (instead of React.lazy)
|
|
17
|
+
// gives each panel a .preload() method so the chunk can be fetched on intent
|
|
18
|
+
// (hovering the sidebar icon or developer-panel tab) before the user clicks.
|
|
19
|
+
|
|
20
|
+
export const LazyTerminal = reactLazyWithPreload(
|
|
21
|
+
() => import("@/components/terminal/terminal"),
|
|
22
|
+
);
|
|
23
|
+
export const LazyChatPanel = reactLazyWithPreload(
|
|
24
|
+
() => import("@/components/chat/chat-panel"),
|
|
25
|
+
);
|
|
26
|
+
export const LazyAgentPanel = reactLazyWithPreload(
|
|
27
|
+
() => import("@/components/chat/acp/agent-panel"),
|
|
28
|
+
);
|
|
29
|
+
export const LazyDependencyGraphPanel = reactLazyWithPreload(
|
|
30
|
+
() => import("../panels/dependency-graph-panel"),
|
|
31
|
+
);
|
|
32
|
+
export const LazySessionPanel = reactLazyWithPreload(
|
|
33
|
+
() => import("../panels/session-panel"),
|
|
34
|
+
);
|
|
35
|
+
export const LazyDocumentationPanel = reactLazyWithPreload(
|
|
36
|
+
() => import("../panels/documentation-panel"),
|
|
37
|
+
);
|
|
38
|
+
export const LazyErrorsPanel = reactLazyWithPreload(
|
|
39
|
+
() => import("../panels/error-panel"),
|
|
40
|
+
);
|
|
41
|
+
export const LazyFileExplorerPanel = reactLazyWithPreload(
|
|
42
|
+
() => import("../panels/file-explorer-panel"),
|
|
43
|
+
);
|
|
44
|
+
export const LazyLogsPanel = reactLazyWithPreload(
|
|
45
|
+
() => import("../panels/logs-panel"),
|
|
46
|
+
);
|
|
47
|
+
export const LazyOutlinePanel = reactLazyWithPreload(
|
|
48
|
+
() => import("../panels/outline-panel"),
|
|
49
|
+
);
|
|
50
|
+
export const LazyPackagesPanel = reactLazyWithPreload(
|
|
51
|
+
() => import("../panels/packages-panel"),
|
|
52
|
+
);
|
|
53
|
+
export const LazyScratchpadPanel = reactLazyWithPreload(
|
|
54
|
+
() => import("../panels/scratchpad-panel"),
|
|
55
|
+
);
|
|
56
|
+
export const LazySecretsPanel = reactLazyWithPreload(
|
|
57
|
+
() => import("../panels/secrets-panel"),
|
|
58
|
+
);
|
|
59
|
+
export const LazySnippetsPanel = reactLazyWithPreload(
|
|
60
|
+
() => import("../panels/snippets-panel"),
|
|
61
|
+
);
|
|
62
|
+
export const LazyTracingPanel = reactLazyWithPreload(
|
|
63
|
+
() => import("../panels/tracing-panel"),
|
|
64
|
+
);
|
|
65
|
+
export const LazyCachePanel = reactLazyWithPreload(
|
|
66
|
+
() => import("../panels/cache-panel"),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Preloader registry: hovering an icon/tab calls into this map to warm the
|
|
70
|
+
// corresponding chunk. Two panel types (chat and agents) share the "ai" slot,
|
|
71
|
+
// so we preload both.
|
|
72
|
+
export const PANEL_PRELOADERS: Record<PanelType, () => void> = {
|
|
73
|
+
files: safePreload(LazyFileExplorerPanel),
|
|
74
|
+
variables: safePreload(LazySessionPanel),
|
|
75
|
+
dependencies: safePreload(LazyDependencyGraphPanel),
|
|
76
|
+
packages: safePreload(LazyPackagesPanel),
|
|
77
|
+
outline: safePreload(LazyOutlinePanel),
|
|
78
|
+
documentation: safePreload(LazyDocumentationPanel),
|
|
79
|
+
snippets: safePreload(LazySnippetsPanel),
|
|
80
|
+
ai: () => {
|
|
81
|
+
safePreload(LazyChatPanel)();
|
|
82
|
+
safePreload(LazyAgentPanel)();
|
|
83
|
+
},
|
|
84
|
+
errors: safePreload(LazyErrorsPanel),
|
|
85
|
+
scratchpad: safePreload(LazyScratchpadPanel),
|
|
86
|
+
tracing: safePreload(LazyTracingPanel),
|
|
87
|
+
secrets: safePreload(LazySecretsPanel),
|
|
88
|
+
logs: safePreload(LazyLogsPanel),
|
|
89
|
+
terminal: safePreload(LazyTerminal),
|
|
90
|
+
cache: safePreload(LazyCachePanel),
|
|
91
|
+
};
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
PANELS,
|
|
23
23
|
type PanelDescriptor,
|
|
24
24
|
} from "../types";
|
|
25
|
+
import { PANEL_PRELOADERS } from "./lazy-panels";
|
|
25
26
|
|
|
26
27
|
export const Sidebar: React.FC = () => {
|
|
27
28
|
const { selectedPanel, selectedDeveloperPanelTab, isSidebarOpen } =
|
|
@@ -141,6 +142,7 @@ export const Sidebar: React.FC = () => {
|
|
|
141
142
|
className="flex flex-col gap-0"
|
|
142
143
|
minItems={0}
|
|
143
144
|
onAction={(panel) => toggleApplication(panel.type)}
|
|
145
|
+
onItemPreloadHint={(panel) => PANEL_PRELOADERS[panel.type]?.()}
|
|
144
146
|
renderItem={(panel) => (
|
|
145
147
|
<SidebarItem
|
|
146
148
|
tooltip={panel.tooltip}
|
|
@@ -51,6 +51,22 @@
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
a {
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
text-decoration: inherit;
|
|
57
|
+
|
|
58
|
+
@apply text-link;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
a:hover,
|
|
62
|
+
a:active {
|
|
63
|
+
text-decoration: underline;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
a:visited {
|
|
67
|
+
@apply text-link-visited;
|
|
68
|
+
}
|
|
69
|
+
|
|
54
70
|
code {
|
|
55
71
|
@apply border px-1 rounded font-mono bg-[var(--slate-2)] text-sm mb-4 mt-2;
|
|
56
72
|
}
|
|
@@ -137,3 +153,22 @@
|
|
|
137
153
|
}
|
|
138
154
|
}
|
|
139
155
|
}
|
|
156
|
+
|
|
157
|
+
/* Syntax highlighting for LSP hover code blocks (lezer classHighlighter tok-* classes) */
|
|
158
|
+
.docs-documentation,
|
|
159
|
+
.cm-tooltip .documentation {
|
|
160
|
+
.tok-keyword { color: light-dark(#708, #c678dd); font-weight: 500; }
|
|
161
|
+
.tok-string,
|
|
162
|
+
.tok-string2 { color: light-dark(#a11, #98c379); }
|
|
163
|
+
.tok-number { color: light-dark(#164, #d19a66); }
|
|
164
|
+
.tok-bool,
|
|
165
|
+
.tok-atom { color: light-dark(#219, #d19a66); }
|
|
166
|
+
.tok-comment { color: var(--cm-comment); }
|
|
167
|
+
.tok-className { color: light-dark(#00f, #61afef); }
|
|
168
|
+
.tok-typeName,
|
|
169
|
+
.tok-namespace { color: light-dark(#085, #56b6c2); }
|
|
170
|
+
.tok-operator { color: light-dark(#a2f, #56b6c2); }
|
|
171
|
+
.tok-propertyName { color: light-dark(#05a, #e5c07b); }
|
|
172
|
+
.tok-variableName { color: light-dark(#000, #abb2bf); }
|
|
173
|
+
.tok-punctuation { color: light-dark(#000, #abb2bf); }
|
|
174
|
+
}
|
|
@@ -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
|
-
<
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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.
|
|
@@ -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,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
|
+
});
|