import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { getPaneSidebarWidth, PaneSidebar, PaneSidebarRow, } from "../components/layout/pane/sidebar"; import { ActionRow } from "../components/ui/action-row"; import { Button } from "../components/ui/button"; import { Badge } from "../components/ui/display"; import { Spinner } from "../components/ui/loading"; import { QueryBar } from "../components/ui/query-bar"; import { EmptyState } from "../components/ui/status"; import { useShortcut } from "../react/input"; import { t, tf } from "../i18n"; import { useThemeColors } from "../theme/theme-context"; import { Box, ScrollBox, Text, TextAttributes, useUiCapabilities, type InputRenderable } from "../ui"; import { isPlainKey } from "../utils/keyboard"; import type { GallerySearchState, LayoutGalleryController } from "./gallery"; import { MiniWorkspace } from "./mini-workspace"; import { describeArrangement, formatPublishedAt, resolvePreviewEntry, summarizeLayoutPanes, type GalleryEntry, } from "./model"; const PREVIEW = { width: 640, height: 320 }; const ELLIPSIS = { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } as const; /** Rows PageUp and PageDown skip. */ const PAGE_STEP = 10; type ScrollableRow = { matches?(selector: string): boolean; scrollIntoView?(options: { block: "nearest" }): void }; type RowKeyEvent = { key?: string; preventDefault?: () => void; stopPropagation?: () => void }; /** * The cursor row scrolls into view when the keyboard moves it. A row the * pointer highlighted stays put, so hovering never slides the list. */ function useCursorRowInView(selected: boolean) { const rowRef = useRef(null); useEffect(() => { const row = rowRef.current; if (!selected || row?.matches?.(":hover")) return; row?.scrollIntoView?.({ block: "nearest" }); }, [selected]); return rowRef; } /** A collapsible group header, the same row the chat sidebar uses. */ function SidebarSection({ title, count, expanded, width, onToggle, }: { title: string; count: number; expanded: boolean; width: number; onToggle: () => void; }) { return ( ); } function SidebarNote({ children }: { children: ReactNode }) { const colors = useThemeColors(); return ( {children} ); } /** A state inside the sidebar with its own action (log in, retry). */ function SidebarState({ children }: { children: ReactNode }) { return ( {children} ); } function EntryRow({ entry, controller, selected, }: { entry: GalleryEntry; controller: LayoutGalleryController; selected: boolean; }) { const colors = useThemeColors(); const rowRef = useCursorRowInView(selected); const panes = summarizeLayoutPanes(entry.layout, controller.panes); const missing = panes.filter((pane) => pane.missing).length; const select = () => controller.select(entry.id); const activate = () => (entry.kind === "owned" ? controller.activate(entry) : controller.install(entry)); return ( { select(); // A double click opens the layout, like Enter. if ((event?.detail ?? 0) >= 2) activate(); }} > {({ foregroundColor, listWidth, onMouseDown }) => ( { // Off the cursor, Enter belongs to the row the cursor is on. if (!selected || (event.key !== "Enter" && event.key !== " ")) return; event.preventDefault?.(); event.stopPropagation?.(); activate(); }} style={{ cursor: "pointer" }} > {entry.active ? " ● " : " "} {entry.name} {missing > 0 && !} )} ); } function DiscoverStatus({ controller }: { controller: LayoutGalleryController }) { const colors = useThemeColors(); const { discover } = controller; if (!controller.signedIn) { return ( {/* The sidebar is narrow, so the pair wraps instead of running under the divider. */}