// Path: Frontend/src/components/shell/ProjectSelector/ProjectSelector.tsx import { For, Show, createEffect, createMemo, createSignal, onCleanup, onMount, type JSX } from "solid-js"; import { ChevronDown, ChevronRight, Folder, LayoutGrid } from "../../../lib/icons"; import { ProjectContextMenu } from "../ProjectContextMenu/ProjectContextMenu"; import { useAppShellData } from "../data/app-shell.context"; import { createProjectFolderTarget, createProjectSurfaceTarget, createProjectTarget, type ProjectItem, type ProjectMenuTarget, } from "../data/shell.data"; import { createProjectContextMenuController } from "../ProjectContextMenu/createProjectContextMenuController"; import styles from "./ProjectSelector.module.scss"; type ProjectSelectorProps = { compact?: boolean; isOpen: boolean; onToggle: () => void; onClose: () => void; }; export const ProjectSelector = (props: ProjectSelectorProps): JSX.Element => { const appShellData = useAppShellData(); const [selectedProject, setSelectedProject] = createSignal(appShellData.activeProject()); const [drawerTop, setDrawerTop] = createSignal(0); const [collapsedFolderIds, setCollapsedFolderIds] = createSignal([]); let rootRef: HTMLDivElement | undefined; let triggerRef: HTMLButtonElement | undefined; let contextMenuRef: HTMLDivElement | undefined; const contextMenu = createProjectContextMenuController(); const projectFolders = createMemo(() => { const sections = new Map(); for (const item of appShellData.projectItems()) { const key = item.parentLabel || item.groupLabel || "Projects"; const existing = sections.get(key); if (existing) { existing.push(item); continue; } sections.set(key, [item]); } return Array.from(sections.entries()).map(([label, items]) => ({ id: label.toLowerCase().replace(/\s+/g, "-"), label, meta: items[0]?.groupLabel && items[0].groupLabel !== label ? items[0].groupLabel : undefined, items, })); }); const isFolderCollapsed = (folderId: string): boolean => collapsedFolderIds().includes(folderId); const toggleFolder = (folderId: string): void => { setCollapsedFolderIds((current) => current.includes(folderId) ? current.filter((id) => id !== folderId) : [...current, folderId], ); }; createEffect(() => { setSelectedProject(appShellData.activeProject()); }); onMount(() => { if (triggerRef) { const updateDrawerTop = (): void => { if (!triggerRef) { return; } setDrawerTop(triggerRef.offsetTop + triggerRef.offsetHeight + 8); }; updateDrawerTop(); const observer = new ResizeObserver(() => { updateDrawerTop(); }); observer.observe(triggerRef); window.addEventListener("resize", updateDrawerTop); onCleanup(() => { observer.disconnect(); window.removeEventListener("resize", updateDrawerTop); }); } const handlePointerDown = (event: PointerEvent): void => { if (!props.isOpen || !rootRef) { return; } const target = event.target; if (target instanceof Node && rootRef.contains(target)) { return; } if (target instanceof Node && contextMenuRef?.contains(target)) { return; } props.onClose(); }; const handleEscape = (event: KeyboardEvent): void => { if (event.key !== "Escape" || !props.isOpen) { return; } props.onClose(); triggerRef?.focus(); }; document.addEventListener("pointerdown", handlePointerDown); window.addEventListener("keydown", handleEscape); onCleanup(() => { document.removeEventListener("pointerdown", handlePointerDown); window.removeEventListener("keydown", handleEscape); }); }); const toggleOpen = (): void => { if (!props.isOpen) { props.onToggle(); return; } props.onClose(); }; const selectProject = (projectId: string): void => { const nextProject = appShellData.projectItems().find((item): boolean => item.id === projectId); if (!nextProject) { return; } setSelectedProject({ id: nextProject.id, name: nextProject.name }); props.onClose(); }; const handleContextActionSelect = (_action: { id: string; label: string }, _target: ProjectMenuTarget): void => { // Initial implementation keeps the project menu aligned with workspace-menu IA. }; const handleSurfaceContextMenu = (event: MouseEvent): void => { event.stopPropagation(); contextMenu.openMenu(event, createProjectSurfaceTarget("Projects")); }; return (
<>
    {(item): JSX.Element => { const isSelected = (): boolean => selectedProject().id === item.id; return (
  • ); }}
); }}
{ const state = contextMenu.menuState(); return state ? { x: state.x, y: state.y } : null; })()} menuRef={(element) => { contextMenuRef = element; contextMenu.setMenuRef(element); }} onClose={contextMenu.closeMenu} onSelect={handleContextActionSelect} /> ); };