/* Copyright 2026 Marimo. All rights reserved. */ import { useAtomValue } from "jotai"; import { Check, Code2Icon, CodeIcon, FolderDownIcon, ImageIcon, Loader2Icon, MoreHorizontalIcon, } from "lucide-react"; import type React from "react"; import { memo, useRef, useState } from "react"; import { z } from "zod"; import { ReadonlyCode } from "@/components/editor/code/readonly-python-code"; import { OutputArea } from "@/components/editor/Output"; import { ConsoleOutput } from "@/components/editor/output/console/ConsoleOutput"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { outputIsLoading, outputIsStale } from "@/core/cells/cell"; import type { CellId } from "@/core/cells/ids"; import type { CellData, CellRuntimeState } from "@/core/cells/types"; import { getReadonlyCodeDisplay } from "@/core/cells/readonly-code-display"; import { MarkdownLanguageAdapter } from "@/core/codemirror/language/languages/markdown"; import { useResolvedMarimoConfig } from "@/core/config/config"; import { CSSClasses } from "@/core/constants"; import { kernelStateAtom } from "@/core/kernel/state"; import { useNotebookCodeAvailable } from "@/core/meta/code-visibility"; import { showCodeInRunModeAtom } from "@/core/meta/state"; import { publishedCellClasses, shouldHidePublishedCell, } from "@/core/cells/utils"; import { type AppMode, kioskModeAtom } from "@/core/mode"; import { useRequestClient } from "@/core/network/requests"; import type { CellConfig } from "@/core/network/types"; import { downloadAsHTML } from "@/core/static/download-html"; import { isStaticNotebook } from "@/core/static/static-state"; import { cn } from "@/utils/cn"; import { ADD_PRINTING_CLASS, downloadBlob, downloadHTMLAsImage, } from "@/utils/download"; import { Filenames } from "@/utils/filenames"; import { FloatingOutline } from "../../chrome/panels/outline/floating-outline"; import { cellDomProps } from "../../common"; import type { ICellRendererPlugin, ICellRendererProps } from "../types"; import { useDelayVisibility } from "./useDelayVisibility"; import { getInitialShowCode, groupCellsByColumn, shouldHideCode, updateShowCodeQueryParam, } from "./utils"; import { VerticalLayoutWrapper } from "./vertical-layout-wrapper"; type VerticalLayout = null; type VerticalLayoutProps = ICellRendererProps; const VerticalLayoutRenderer: React.FC = ({ cells, appConfig, mode, }) => { const { invisible } = useDelayVisibility(cells.length, mode); const kioskMode = useAtomValue(kioskModeAtom); const kernelState = useAtomValue(kernelStateAtom); const [userConfig] = useResolvedMarimoConfig(); const showCodeInRunModePreference = useAtomValue(showCodeInRunModeAtom); const [showCode, setShowCode] = useState(() => getInitialShowCode({ showCodeInRunModePreference, kioskMode }), ); const canShowCode = useNotebookCodeAvailable(cells); const handleToggleShowCode = () => { setShowCode((currentShowCode) => { const nextShowCode = !currentShowCode; updateShowCodeQueryParam(nextShowCode); return nextShowCode; }); }; const renderCell = (cell: CellRuntimeState & CellData) => { return ( ); }; const renderCells = () => { if (appConfig.width === "columns") { const sortedColumns = groupCellsByColumn(cells); return (
{sortedColumns.map(([columnIndex, columnCells]) => (
{columnCells.map(renderCell)}
))}
); } if (cells.length === 0 && !invisible) { // If kernel is not yet instantiated, show loading state if (!kernelState.isInstantiated) { return (
); } // Kernel is ready but no cells - truly empty notebook return (
Empty Notebook This notebook has no code or outputs.
); } return <>{cells.map(renderCell)}; }; // in read mode (required for canShowCode to be true), we need to insert // spacing between cells to prevent them from colliding; in edit mode, // spacing is handled elsewhere return (
{renderCells()}
{mode === "read" && ( )}
); }; const ActionButtons: React.FC<{ canShowCode: boolean; showCode: boolean; onToggleShowCode: () => void; }> = ({ canShowCode, showCode, onToggleShowCode }) => { const { readCode } = useRequestClient(); const handleDownloadAsPNG = async () => { const app = document.getElementById("App"); if (!app) { return; } await downloadHTMLAsImage({ element: app, filename: document.title, // Add body.printing ONLY when converting the whole notebook to a screenshot prepare: ADD_PRINTING_CLASS, }); }; const handleDownloadAsHTML = async () => { const app = document.getElementById("App"); if (!app) { return; } await downloadAsHTML({ includeCode: true }); }; const handleDownloadAsPython = async () => { const code = await readCode(); downloadBlob( new Blob([code.contents], { type: "text/plain" }), Filenames.toPY(document.title), ); }; const isStatic = isStaticNotebook(); const actions: React.ReactNode[] = []; if (canShowCode) { actions.push( Show code {showCode && } , , ); } if (!isStatic) { actions.push( Download as HTML , ); // Only show download as Python if code is available if (canShowCode) { actions.push( Download notebook source , ); } actions.push( , Download as PNG , ); } if (actions.length === 0) { return null; } // Don't change the id of this element // as this may be used in custom css to hide/show the actions dropdown return (
{actions}
); }; interface VerticalCellProps extends Pick< CellRuntimeState, | "output" | "consoleOutputs" | "status" | "stopped" | "errored" | "interrupted" | "staleInputs" | "runStartTimestamp" > { cellOutputArea: "above" | "below"; cellId: CellId; config: CellConfig; code: string; mode: AppMode; showCode: boolean; name: string; kiosk: boolean; showErrorTracebacks: boolean; } const VerticalCell = memo( ({ output, consoleOutputs, cellOutputArea, cellId, status, stopped, errored, config, interrupted, staleInputs, runStartTimestamp, code, showCode, mode, name, kiosk, showErrorTracebacks, }: VerticalCellProps) => { const cellRef = useRef(null); const outputStale = outputIsStale( { status, output, interrupted, runStartTimestamp, staleInputs, }, false, ); const loading = outputIsLoading(status); // Kiosk and not presenting const kioskFull = kiosk && mode !== "present"; const isPureMarkdown = new MarkdownLanguageAdapter().isSupported(code); const published = !showCode && !kioskFull; const className = cn( "marimo-cell", "hover-actions-parent empty:invisible", published ? publishedCellClasses({ errored, stopped }) : { "has-error": errored, stopped: stopped, borderless: isPureMarkdown, }, ); // Read mode and show code if ((mode === "read" && showCode) || kioskFull) { const outputArea = ( ); // Hide the code if it's pure markdown and there's an output, or if the code is empty const hideCode = shouldHideCode(code, output); // Only unwrap SQL when the code will actually be rendered. const display = hideCode ? null : getReadonlyCodeDisplay(code); return (
{cellOutputArea === "above" && outputArea} {display && (
)} {cellOutputArea === "below" && outputArea} null} cellId={cellId} debuggerActive={false} />
); } // When show_tracebacks is enabled, show error outputs inline // instead of hiding them const hidden = shouldHidePublishedCell({ errored, interrupted, stopped, output, showErrorTracebacks, }); if (hidden) { return null; } return (
); }, ); VerticalCell.displayName = "VerticalCell"; export const VerticalLayoutPlugin: ICellRendererPlugin< VerticalLayout, VerticalLayout > = { type: "vertical", name: "Vertical", validator: z.any(), Component: VerticalLayoutRenderer, deserializeLayout: (serialized) => serialized, serializeLayout: (layout) => layout, getInitialLayout: () => null, };