diff --git a/js/desktop/src/features/particles/stream-view.tsx b/js/desktop/src/features/particles/stream-view.tsx index 6abe7e3..e309990 100644 --- a/js/desktop/src/features/particles/stream-view.tsx +++ b/js/desktop/src/features/particles/stream-view.tsx @@ -277,6 +277,7 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) { const [prevParticleId, setPrevParticleId] = useState(currentParticle?.id); const [showKeybindings, setShowKeybindings] = useState(false); const [textReactionOpen, setTextReactionOpen] = useState(false); + const [taskEditing, setTaskEditing] = useState(false); const handleSubmitTextReaction = useCallback( (text: string) => { @@ -291,6 +292,12 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) { navigate(`/${networkId}`); }, [navigate, networkId]); + // Leaving task edit focus: blur the active field, which clears the card's + // editing state and resumes playback. + const handleExitEditing = useCallback(() => { + (document.activeElement as HTMLElement | null)?.blur(); + }, []); + useStreamNavigationKeys({ next, prev, @@ -299,6 +306,8 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) { mediaRef, onExit: handleExitNavigate, onToggleViewMode: toggleViewMode, + editing: taskEditing, + onExitEditing: handleExitEditing, }); const handleOpenHuddle = useCallback(() => { @@ -449,6 +458,8 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) { paused={paused} onEnded={handleParticleEnded} onProgress={setProgress} + immersive + onEditingChange={setTaskEditing} /> ); default: diff --git a/js/desktop/src/features/particles/task-particle-view.tsx b/js/desktop/src/features/particles/task-particle-view.tsx index cc73772..2cb967f 100644 --- a/js/desktop/src/features/particles/task-particle-view.tsx +++ b/js/desktop/src/features/particles/task-particle-view.tsx @@ -37,6 +37,10 @@ interface TaskParticleViewProps { paused: boolean; onEnded: () => void; onProgress?: (ratio: number) => void; + /** Dim and blur the surrounding stream chrome while a field is focused. */ + immersive?: boolean; + /** Notify the parent when focus enters or leaves the card (focus mode). */ + onEditingChange?: (editing: boolean) => void; } const DWELL_DURATION_S = 8; @@ -56,6 +60,8 @@ export function TaskParticleView({ paused, onEnded, onProgress, + immersive = false, + onEditingChange, }: TaskParticleViewProps) { const { networkId } = parseParticlePath(containerPath); const network = useNetwork(networkId); @@ -74,6 +80,19 @@ export function TaskParticleView({ const [editing, setEditing] = useState(false); useSuspendPlayback(editing, `task-edit-${particle.id}`); + // Mirror focus state to the stream so it can lock navigation and route + // Escape to "leave the editor" instead of "exit the stream". Reset on + // unmount so a particle change mid-edit doesn't strand the stream in + // focus mode. + useEffect(() => { + onEditingChange?.(editing); + return () => onEditingChange?.(false); + }, [editing, onEditingChange]); + + const exitFocus = useCallback(() => { + (document.activeElement as HTMLElement | null)?.blur(); + }, []); + useFixedDwell({ id: particle.id, durationS: DWELL_DURATION_S, @@ -166,8 +185,20 @@ export function TaskParticleView({ return (