/* Copyright 2026 Marimo. All rights reserved. */ import { SearchIcon } from "lucide-react"; import type React from "react"; import { Suspense, useMemo, useState } from "react"; import { ErrorBoundary } from "@/components/editor/boundary/ErrorBoundary"; import { Spinner } from "@/components/icons/spinner"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { getSessionId } from "@/core/kernel/session"; import { useRequestClient } from "@/core/network/requests"; import { useAsyncData } from "@/hooks/useAsyncData"; import { Banner } from "@/plugins/impl/common/error-banner"; import { prettyError } from "@/utils/errors"; import { PathBuilder, Paths } from "@/utils/paths"; import { asURL } from "@/utils/url"; const capitalize = (word: string): string => { return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); }; const titleCase = (path: string): string => { const delimiter = PathBuilder.guessDeliminator(path).deliminator; return path .replace(/\.[^./]+$/, "") .split(delimiter) .filter(Boolean) .map((part) => part.split(/[_-]/).map(capitalize).join(" ")) .join(" > "); }; const tabTarget = (path: string): string => { return `${getSessionId()}-${encodeURIComponent(path)}`; }; const isHttpsUrl = (value: string): boolean => { try { const url = new URL(value); return url.protocol === "https:"; } catch { return false; } }; const SEARCH_THRESHOLD = 10; const GalleryPage: React.FC = () => { const { getWorkspaceFiles } = useRequestClient(); const [searchQuery, setSearchQuery] = useState(""); const response = useAsyncData( () => getWorkspaceFiles({ includeMarkdown: false }), [], ); const workspace = response.data; const formattedFiles = useMemo(() => { const files = workspace?.files ?? []; const root = workspace?.root ?? ""; return files .filter((file) => !file.isDirectory) .map((file) => { const relativePath = root && Paths.isAbsolute(file.path) && file.path.startsWith(root) ? Paths.rest(file.path, root) : file.path; const title = file.opengraph?.title ?? titleCase(Paths.basename(relativePath)); const subtitle = titleCase(Paths.dirname(relativePath)); const description = file.opengraph?.description ?? ""; const opengraphImage = file.opengraph?.image; const thumbnailUrl = opengraphImage && isHttpsUrl(opengraphImage) ? opengraphImage : asURL( `og/thumbnail?file=${encodeURIComponent(relativePath)}`, ).toString(); return { ...file, relativePath, title, subtitle, description, thumbnailUrl, }; }) .toSorted((a, b) => a.relativePath.localeCompare(b.relativePath)); }, [workspace?.files, workspace?.root]); const filteredFiles = useMemo(() => { if (!searchQuery) { return formattedFiles; } const query = searchQuery.toLowerCase(); return formattedFiles.filter((file) => file.title.toLowerCase().includes(query), ); }, [formattedFiles, searchQuery]); if (response.isPending) { return ; } if (response.error) { return ( {prettyError(response.error)} ); } if (!workspace) { return ; } return (
marimo logo
{workspace.hasMore && ( Showing first {workspace.fileCount} files. Your workspace has more files. )} {formattedFiles.length > SEARCH_THRESHOLD && ( } onChange={(event) => setSearchQuery(event.target.value)} placeholder="Search" rootClassName="mb-3" className="mb-0 border-border" /> )} {filteredFiles.length === 0 ? ( No marimo apps found. ) : ( )}
); }; export default GalleryPage;