| 1 | import { lazy, Suspense, type ComponentProps, type KeyboardEvent, type PointerEvent, type ReactNode } from "react"; |
| 2 | import { AlarmClock, MessageSquare, Search, Settings, Trash2 } from "lucide-react"; |
| 3 | import { Tooltip } from "../components/Tooltip"; |
| 4 | import type { Translator } from "../lib/i18n"; |
| 5 | import type { SettingsTab } from "../lib/types"; |
| 6 | import logoWordmark from "../assets/logo-wordmark.svg"; |
| 7 | |
| 8 | const ProjectTree = lazy(() => import("../components/ProjectTree").then((module) => ({ default: module.ProjectTree }))); |
| 9 | |
| 10 | export type SidebarRegionProps = { |
| 11 | className: string; |
| 12 | collapsed: boolean; |
| 13 | resize: { |
| 14 | min: number; |
| 15 | max: number; |
| 16 | value: number; |
| 17 | onPointerDown: (event: PointerEvent<HTMLButtonElement>) => void; |
| 18 | onKeyDown: (event: KeyboardEvent<HTMLButtonElement>) => void; |
| 19 | onReset: () => void; |
| 20 | }; |
| 21 | projectTree: ComponentProps<typeof ProjectTree>; |
| 22 | t: Translator; |
| 23 | onNewSession: () => void; |
| 24 | onOpenPalette: () => void; |
| 25 | paletteShortcut: string; |
| 26 | onOpenTrash: () => void; |
| 27 | onOpenAutomation: () => void; |
| 28 | onOpenSettings: (tab: SettingsTab) => void; |
| 29 | }; |
| 30 | |
| 31 | /** Workbench sidebar: brand, quick new-session, project tree and utility nav. */ |
| 32 | export function SidebarRegion(props: SidebarRegionProps) { |
| 33 | const { t } = props; |
| 34 | return ( |
| 35 | <> |
| 36 | <aside className={props.className} aria-label={t("sidebar.navigation")}> |
| 37 | <div className="sidebar__head" aria-hidden={props.collapsed}> |
| 38 | <div className="sidebar__brand sidebar__brand--workbench"> |
| 39 | <img src={logoWordmark} alt="Reasonix" className="sidebar__brand-logo sidebar__brand-logo--workbench" draggable={false} /> |
| 40 | </div> |
| 41 | </div> |
| 42 | <div className="sidebar__quick-actions"> |
| 43 | <button className="sidebar__quick-action" type="button" onClick={props.onNewSession}> |
| 44 | <MessageSquare size={18} aria-hidden="true" /><span>{t("topbar.newSession")}</span> |
| 45 | </button> |
| 46 | </div> |
| 47 | <section className="sidebar__section sidebar__section--projects"> |
| 48 | <Suspense fallback={null}><ProjectTree {...props.projectTree} /></Suspense> |
| 49 | </section> |
| 50 | <nav className="sidebar__nav sidebar__nav--footer"> |
| 51 | <div className="sidebar__utility-row" aria-label={t("sidebar.utilityActions")}> |
| 52 | <UtilityButton |
| 53 | label={t("shortcuts.action.commandPalette")} |
| 54 | tooltip={`${t("shortcuts.action.commandPalette")} ${props.paletteShortcut}`} |
| 55 | icon={<Search size={16} />} |
| 56 | onClick={props.onOpenPalette} |
| 57 | /> |
| 58 | <UtilityButton label={t("sidebar.trash")} icon={<Trash2 size={16} />} onClick={props.onOpenTrash} /> |
| 59 | <UtilityButton label={t("heartbeat.scheduler")} icon={<AlarmClock size={16} />} onClick={props.onOpenAutomation} /> |
| 60 | <UtilityButton label={t("topbar.settings")} icon={<Settings size={16} />} onClick={() => props.onOpenSettings("general")} /> |
| 61 | </div> |
| 62 | </nav> |
| 63 | </aside> |
| 64 | <button className="sidebar-resizer" type="button" role="separator" aria-orientation="vertical" aria-label={t("sidebar.resize")} |
| 65 | aria-valuemin={props.resize.min} aria-valuemax={props.resize.max} aria-valuenow={props.resize.value} |
| 66 | onPointerDown={props.resize.onPointerDown} onKeyDown={props.resize.onKeyDown} onDoubleClick={props.resize.onReset} /> |
| 67 | </> |
| 68 | ); |
| 69 | } |
| 70 | |
| 71 | function UtilityButton({ icon, label, tooltip = label, onClick }: { icon: ReactNode; label: string; tooltip?: string; onClick: () => void }) { |
| 72 | return <Tooltip label={tooltip} fill side="top"><button className="sidebar__utility-button" type="button" aria-label={label} onClick={onClick}>{icon}<span className="sr-only">{label}</span></button></Tooltip>; |
| 73 | } |
| 74 |