diff --git a/frontend/src/components/EditorLayout.tsx b/frontend/src/components/EditorLayout.tsx index b36caf74..7252dab4 100644 --- a/frontend/src/components/EditorLayout.tsx +++ b/frontend/src/components/EditorLayout.tsx @@ -597,6 +597,7 @@ export default function EditorLayout() { setStackStatuses(prev => ({ ...prev, [stackFile]: status })); }; + // Stable identity required: captured by buildMenuCtx's memoization and passed as a prop; unstable refs cause descendant re-render churn. const refreshLabels = useCallback(async () => { if (!isPaid) return; try { diff --git a/frontend/src/components/LabelAssignPopover.tsx b/frontend/src/components/LabelAssignPopover.tsx deleted file mode 100644 index 64b98944..00000000 --- a/frontend/src/components/LabelAssignPopover.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { useState } from 'react'; -import { Check, Plus } from 'lucide-react'; -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { ScrollArea } from '@/components/ui/scroll-area'; -import { apiFetch } from '@/lib/api'; -import { toast } from '@/components/ui/toast-store'; -import { LabelDot } from './LabelPill'; -import { LABEL_COLORS, MAX_LABELS_PER_NODE, type Label, type LabelColor } from './label-types'; - -interface LabelAssignPopoverProps { - stackName: string; - allLabels: Label[]; - assignedLabelIds: number[]; - onLabelsChanged: () => void; - children: React.ReactNode; -} - -export function LabelAssignPopover({ stackName, allLabels, assignedLabelIds, onLabelsChanged, children }: LabelAssignPopoverProps) { - const [open, setOpen] = useState(false); - const [creating, setCreating] = useState(false); - const [newName, setNewName] = useState(''); - const [newColor, setNewColor] = useState('teal'); - const [saving, setSaving] = useState(false); - - const toggleLabel = async (labelId: number) => { - const current = new Set(assignedLabelIds); - if (current.has(labelId)) { - current.delete(labelId); - } else { - current.add(labelId); - } - try { - const res = await apiFetch(`/stacks/${encodeURIComponent(stackName)}/labels`, { - method: 'PUT', - body: JSON.stringify({ labelIds: Array.from(current) }), - }); - if (!res.ok) { - const data = await res.json().catch(() => ({})); - throw new Error(data?.error || 'Failed to update labels.'); - } - onLabelsChanged(); - } catch (err: unknown) { - toast.error((err as Error)?.message || 'Failed to update labels.'); - } - }; - - const createAndAssign = async () => { - if (!newName.trim()) return; - setSaving(true); - try { - const res = await apiFetch('/labels', { - method: 'POST', - body: JSON.stringify({ name: newName.trim(), color: newColor }), - }); - if (!res.ok) { - const data = await res.json().catch(() => ({})); - throw new Error(data?.error || 'Failed to create label.'); - } - const label: Label = await res.json(); - const newIds = [...assignedLabelIds, label.id]; - const assignRes = await apiFetch(`/stacks/${encodeURIComponent(stackName)}/labels`, { - method: 'PUT', - body: JSON.stringify({ labelIds: newIds }), - }); - if (!assignRes.ok) { - const data = await assignRes.json().catch(() => ({})); - throw new Error(data?.error || 'Failed to assign label.'); - } - onLabelsChanged(); - setCreating(false); - setNewName(''); - setNewColor('teal'); - } catch (err: unknown) { - toast.error((err as Error)?.message || 'Failed to create label.'); - } finally { - setSaving(false); - } - }; - - return ( - - - {children} - - -
Labels
- -
- {allLabels.map(label => ( - - ))} - {allLabels.length === 0 && !creating && ( -
No labels yet.
- )} -
-
- {creating ? ( -
- setNewName(e.target.value)} - className="h-7 text-xs font-mono" - maxLength={30} - autoFocus - onKeyDown={e => { if (e.key === 'Enter') createAndAssign(); if (e.key === 'Escape') setCreating(false); }} - /> -
- {LABEL_COLORS.map(c => ( -
-
- - -
-
- ) : allLabels.length < MAX_LABELS_PER_NODE ? ( - - ) : null} -
-
- ); -} diff --git a/frontend/src/components/ui/context-menu.tsx b/frontend/src/components/ui/context-menu.tsx index f2810ad9..a9f77071 100644 --- a/frontend/src/components/ui/context-menu.tsx +++ b/frontend/src/components/ui/context-menu.tsx @@ -37,6 +37,7 @@ const ContextMenuSubTrigger = React.forwardRef< )) ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName +// Portal-wrapped so sub-menus escape ancestors with overflow-x-hidden. Keep the Portal. const ContextMenuSubContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef diff --git a/frontend/src/components/ui/dropdown-menu.tsx b/frontend/src/components/ui/dropdown-menu.tsx index 92c10681..76da4450 100644 --- a/frontend/src/components/ui/dropdown-menu.tsx +++ b/frontend/src/components/ui/dropdown-menu.tsx @@ -38,6 +38,7 @@ const DropdownMenuSubTrigger = React.forwardRef< DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName +// Portal-wrapped so sub-menus escape ancestors with overflow-x-hidden (e.g. the sidebar kebab container). Keep the Portal. const DropdownMenuSubContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef