/* Copyright 2026 Marimo. All rights reserved. */ import { useAtomValue } from "jotai"; import React from "react"; import { notebookOutline } from "@/core/cells/cells"; import type { OutlineItem } from "@/core/cells/outline"; import { cn } from "@/utils/cn"; import { findOutlineElements, scrollToOutlineItem, useActiveOutline, } from "./useActiveOutline"; export const FloatingOutline: React.FC = () => { const { items } = useAtomValue(notebookOutline); const { activeHeaderId, activeOccurrences } = useActiveOutline( findOutlineElements(items), ); const [isHovered, setIsHovered] = React.useState(false); // Hide if < 2 items // It's kinda useless to have an outline with only one item // and Notion does the same if (items.length < 2) { return null; } return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} className={cn( "fixed top-[25vh] right-8 z-10000 print:hidden", // Hide on small screens "hidden md:block", )} >
); }; export const MiniMap: React.FC<{ items: OutlineItem[]; activeHeaderId: string | undefined; activeOccurrences: number | undefined; }> = ({ items, activeHeaderId, activeOccurrences }) => { // Map of selector to its occurrences const seen = new Map(); return (
{items.map((item, idx) => { const identifier = "id" in item.by ? item.by.id : item.by.path; // Keep track of how many times we've seen this selector const occurrences = seen.get(identifier) ?? 0; seen.set(identifier, occurrences + 1); return (
scrollToOutlineItem(item, occurrences)} /> ); })}
); }; export const OutlineList: React.FC<{ className?: string; items: OutlineItem[]; activeHeaderId: string | undefined; activeOccurrences: number | undefined; }> = ({ items, activeHeaderId, activeOccurrences, className }) => { // Map of selector to its occurrences const seen = new Map(); return (
{items.map((item, idx) => { const identifier = "id" in item.by ? item.by.id : item.by.path; // Keep track of how many times we've seen this selector const occurrences = seen.get(identifier) ?? 0; seen.set(identifier, occurrences + 1); const key = `${identifier}-${idx}`; const sharedProps = { className: cn( "px-2 py-1 cursor-pointer hover:bg-accent/50 hover:text-accent-foreground rounded-l", item.level === 1 && "font-semibold", item.level === 2 && "ml-3", item.level === 3 && "ml-6", item.level === 4 && "ml-9", occurrences === activeOccurrences && activeHeaderId === identifier && "text-accent-foreground", ), onClick: () => scrollToOutlineItem(item, occurrences), }; if (item.html) { return (
); } return (
{item.name}
); })}
); };