diff --git a/src/shared-ui/HierarchicalMenu.tsx b/src/shared-ui/HierarchicalMenu.tsx new file mode 100644 index 00000000..266a78a7 --- /dev/null +++ b/src/shared-ui/HierarchicalMenu.tsx @@ -0,0 +1,104 @@ +import type { CSSProperties, PropsWithChildren } from "react"; + +export type HierarchicalMenuPosition = { + x: number; + y: number; +}; + +export type HierarchicalMenuItem = + | { + kind: "action"; + label: string; + onSelect: () => void; + testId?: string; + disabled?: boolean; + } + | { + kind: "group"; + label: string; + testId?: string; + children: HierarchicalMenuItem[]; + } + | { + kind: "separator"; + }; + +interface HierarchicalMenuProps extends PropsWithChildren { + title: string; + position: HierarchicalMenuPosition; + items: HierarchicalMenuItem[]; + onClose(): void; +} + +function clampMenuPosition(position: HierarchicalMenuPosition): HierarchicalMenuPosition { + const horizontalPadding = 12; + const verticalPadding = 12; + const estimatedMenuWidth = 300; + const estimatedMenuHeight = 420; + + return { + x: Math.max(horizontalPadding, Math.min(position.x, window.innerWidth - estimatedMenuWidth - horizontalPadding)), + y: Math.max(verticalPadding, Math.min(position.y, window.innerHeight - estimatedMenuHeight - verticalPadding)) + }; +} + +function renderHierarchicalMenuItems(items: HierarchicalMenuItem[], onClose: () => void): React.ReactNode { + return items.map((item, index) => { + if (item.kind === "separator") { + return
; + } + + if (item.kind === "group") { + return ( +