'use client'; /** * ActionPicker — portal popover listing the cue types insertable at a given * timeline slot (旁白/聚光/激光/讨论). Shared by the header "add" pill and the * inline "+" affordance (wired in Task 5); this component only renders the * list and reports the chosen type back via `onSelect`. * * Mirrors `CueTooltip`'s `createPortal` pattern in ActionsBar.tsx: a small * card anchored to a DOMRect, clamped to the viewport, dismissed by a * backdrop click or Esc. */ import { useEffect } from 'react'; import { createPortal } from 'react-dom'; import type { Action } from '@/lib/types/action'; import type { SceneType } from '@/lib/types/stage'; import { useI18n } from '@/lib/hooks/use-i18n'; import { cn } from '@/lib/utils/cn'; import { cueLabel, cueMeta } from './cue-meta'; import { pickerOptions, type PickerType } from './picker-options'; const DESC_KEY: Record = { speech: 'edit.picker.speechDesc', spotlight: 'edit.picker.spotlightDesc', laser: 'edit.picker.laserDesc', discussion: 'edit.picker.discussionDesc', }; export function ActionPicker({ anchor, sceneType, actions, onSelect, onClose, }: { anchor: DOMRect; sceneType: SceneType; actions: Action[]; onSelect: (type: PickerType) => void; onClose: () => void; }) { const { t } = useI18n(); useEffect(() => { const onKey = (e: KeyboardEvent) => e.key === 'Escape' && onClose(); window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [onClose]); if (typeof document === 'undefined') return null; const W = 240; const left = Math.min(Math.max(8, anchor.left), window.innerWidth - W - 8); const below = anchor.bottom + 8; const opensUp = below + 220 > window.innerHeight; const options = pickerOptions(sceneType, actions); return createPortal( <>
{t('edit.picker.title')}
{options.map((opt, i) => { const meta = cueMeta(opt.type); const Icon = meta.icon; return (
{opt.type === 'discussion' && i > 0 &&
}
); })}
, document.body, ); }