/* Copyright 2026 Marimo. All rights reserved. */ import { EyeOffIcon, LayoutTemplateIcon, type LucideIcon, Rows2Icon, CookieIcon, PanelRightCloseIcon, PanelRightOpenIcon, KeyboardIcon, } from "lucide-react"; import { Switch } from "@/components/ui/switch"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import type { CellId } from "@/core/cells/ids"; import { cn } from "@/utils/cn"; import type { DeckVerticalAlign, DeckTransition, SlidesLayout, SlideType, } from "../editor/renderers/slides-layout/types"; import { useAtom } from "jotai"; import { atomWithStorage } from "jotai/utils"; import { Tooltip } from "../ui/tooltip"; import { Button } from "../ui/button"; import { Kbd } from "../ui/kbd"; import type { RuntimeCell } from "@/core/cells/types"; import { jotaiJsonStorage } from "@/utils/storage/jotai"; export const DEFAULT_SLIDE_TYPE: SlideType = "slide"; export const DEFAULT_DECK_TRANSITION: DeckTransition = "slide"; export const DEFAULT_DECK_VERTICAL_ALIGN: DeckVerticalAlign = "center"; const COLLAPSED_CONFIG_WIDTH = 36; const slideConfigOpenAtom = atomWithStorage( "marimo:slides:config-open", true, jotaiJsonStorage, { getOnInit: true }, ); export interface SlideTypeOption { value: SlideType; label: string; description: string; Icon: LucideIcon; } export const SLIDE_TYPE_OPTIONS: readonly SlideTypeOption[] = [ { value: "slide", label: "Slide", description: "A new top-level slide. Advances horizontally with the right arrow.", Icon: LayoutTemplateIcon, }, { value: "sub-slide", label: "Sub-slide", description: "Stacks vertically under the previous slide. Reached with the down arrow.", Icon: Rows2Icon, }, { value: "fragment", label: "Fragment", description: "Reveals step-by-step on the current slide without advancing.", Icon: CookieIcon, }, { value: "skip", label: "Skip", description: "Hidden from the presentation. Still visible here in the editor.", Icon: EyeOffIcon, }, ]; /** * Lookup form of {@link SLIDE_TYPE_OPTIONS} for O(1) access by `SlideType`. */ export const SLIDE_TYPE_OPTIONS_BY_VALUE: Readonly< Record > = Object.fromEntries( SLIDE_TYPE_OPTIONS.map((option) => [option.value, option]), ) as Record; interface DeckTransitionOption { value: DeckTransition; label: string; description: string; } const DECK_TRANSITION_OPTIONS: DeckTransitionOption[] = [ { value: "none", label: "None", description: "No animation between slides." }, { value: "fade", label: "Fade", description: "Cross-fade between slides." }, { value: "slide", label: "Slide", description: "Slides move horizontally / vertically.", }, { value: "convex", label: "Convex", description: "Rotate with a convex curve.", }, { value: "concave", label: "Concave", description: "Rotate with a concave curve.", }, { value: "zoom", label: "Zoom", description: "Zoom into the next slide." }, ]; interface DeckVerticalAlignOption { value: DeckVerticalAlign; label: string; description: string; } const DECK_VERTICAL_ALIGN_OPTIONS: DeckVerticalAlignOption[] = [ { value: "center", label: "Center", description: "Vertically center each slide's content.", }, { value: "top", label: "Top", description: "Align content to the top, like the cell view.", }, { value: "bottom", label: "Bottom", description: "Align content to the bottom of each slide.", }, ]; const SlidesForm = ({ layout, setLayout, cellId, }: { layout: SlidesLayout; setLayout: (layout: SlidesLayout) => void; cellId: CellId; }) => { return ( Slide Deck
); }; const KEYBOARD_TIPS: { keys: string[]; description: string }[] = [ { keys: ["F"], description: "Enter fullscreen" }, { keys: ["C"], description: "Toggle code editor" }, ]; const KEYBOARD_SHORTCUTS_URL = "https://vlaaad.github.io/reveal/keyboard-shortcuts"; const KeyboardTips = () => { return (
Shortcuts
    {KEYBOARD_TIPS.map(({ keys, description }) => (
  • {description} {keys.map((key) => ( {key} ))}
  • ))}
See all shortcuts
); }; const SlideConfigForm = ({ layout, setLayout, cellId, }: { layout: SlidesLayout; setLayout: (layout: SlidesLayout) => void; cellId: CellId; }) => { const currentConfig = layout.cells.get(cellId); const currentSlideType: SlideType = currentConfig?.type ?? DEFAULT_SLIDE_TYPE; const showCode = currentConfig?.showCode ?? false; const handleSlideTypeChange = (value: SlideType) => { const newCells = new Map(layout.cells); newCells.set(cellId, { ...currentConfig, type: value }); setLayout({ ...layout, cells: newCells, }); }; const handleShowCodeChange = (checked: boolean) => { const newCells = new Map(layout.cells); newCells.set(cellId, { ...currentConfig, showCode: checked }); setLayout({ ...layout, cells: newCells, }); }; return (
Slide type handleSlideTypeChange(value as SlideType)} className="flex flex-col gap-1.5" > {SLIDE_TYPE_OPTIONS.map(({ value, label, description, Icon }) => { const isSelected = currentSlideType === value; return (

{label}

{description}

); })}
); }; const DeckConfigForm = ({ layout, setLayout, }: { layout: SlidesLayout; setLayout: (layout: SlidesLayout) => void; }) => { const currentTransition: DeckTransition = layout.deck?.transition ?? DEFAULT_DECK_TRANSITION; const activeDescription = DECK_TRANSITION_OPTIONS.find( (opt) => opt.value === currentTransition, )?.description; const currentVerticalAlign: DeckVerticalAlign = layout.deck?.verticalAlign ?? DEFAULT_DECK_VERTICAL_ALIGN; const activeVerticalAlignDescription = DECK_VERTICAL_ALIGN_OPTIONS.find( (opt) => opt.value === currentVerticalAlign, )?.description; const handleTransitionChange = (value: DeckTransition) => { setLayout({ ...layout, deck: { ...layout.deck, transition: value }, }); }; const handleVerticalAlignChange = (value: DeckVerticalAlign) => { setLayout({ ...layout, deck: { ...layout.deck, verticalAlign: value }, }); }; return (
{activeDescription && (

{activeDescription}

)}
{activeVerticalAlignDescription && (

{activeVerticalAlignDescription}

)}
); }; export const SlideSidebar = ({ configWidth, layout, setLayout, activeConfigCell, }: { configWidth: number; layout: SlidesLayout; setLayout: (layout: SlidesLayout) => void; activeConfigCell?: RuntimeCell; }) => { const [isConfigOpen, setIsConfigOpen] = useAtom(slideConfigOpenAtom); return ( // oxlint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- Prevent slide navigation while using controls in the sidebar. ); };