/** * Shared title + save status block (same for Logos and Flux). Gets save status and Save from * the active slot (RecollectionActionsContext); Import/Export from layout-level handlers. */ import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { useParams } from 'react-router-dom' import { usePlatform } from '@/app/kosmos/KosmosContext' import { useRecollectionMenubar } from './RecollectionMenubarContext' import { useRecollectionActions } from './RecollectionActionsContext' import { CheckCircle2, CircleDot, Loader2 } from 'lucide-react' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' const SAVE_KEYS = { key: 's', shiftKey: false } function matchKey(ev: KeyboardEvent, want: { key: string; shiftKey: boolean }) { const mod = ev.ctrlKey || ev.metaKey return ev.key.toLowerCase() === want.key && !!mod && !!ev.shiftKey === want.shiftKey } /** Registers the shared title + save dropdown as titleContent. Renders nothing. */ export function RecollectionTitleContent() { const { setTitleContent } = useRecollectionMenubar() const { activeSlot } = useRecollectionActions() const { recollectionId } = useParams<{ recollectionId: string }>() const { recollections } = usePlatform() const recollectionName = useMemo( () => (recollectionId ? recollections.find((p) => p.id === recollectionId)?.name ?? null : null), [recollectionId, recollections] ) const [showSavedBriefly, setShowSavedBriefly] = useState(false) const savedBrieflyTimerRef = useRef | null>(null) const prevSaveStatusRef = useRef('saved') const saveStatus = activeSlot?.saveStatus ?? 'saved' const onSave = activeSlot?.onSave const canSave = activeSlot?.canSave ?? false useEffect(() => { if (saveStatus === 'unsaved') { setShowSavedBriefly(false) if (savedBrieflyTimerRef.current) { clearTimeout(savedBrieflyTimerRef.current) savedBrieflyTimerRef.current = null } } else if (prevSaveStatusRef.current === 'saving' && saveStatus === 'saved') { setShowSavedBriefly(true) if (savedBrieflyTimerRef.current) clearTimeout(savedBrieflyTimerRef.current) savedBrieflyTimerRef.current = setTimeout(() => { savedBrieflyTimerRef.current = null setShowSavedBriefly(false) }, 2500) } prevSaveStatusRef.current = saveStatus }, [saveStatus]) useEffect(() => { return () => { if (savedBrieflyTimerRef.current) clearTimeout(savedBrieflyTimerRef.current) } }, []) useEffect(() => { const onKeyDown = (ev: KeyboardEvent) => { if (matchKey(ev, SAVE_KEYS) && onSave && canSave) { ev.preventDefault() ev.stopPropagation() onSave() } } window.addEventListener('keydown', onKeyDown, true) return () => window.removeEventListener('keydown', onKeyDown, true) }, [onSave, canSave]) const trigger = useMemo( () => ( {recollectionName ?? 'Untitled'} {saveStatus === 'saving' && } {saveStatus === 'unsaved' && } {saveStatus !== 'unsaved' && (saveStatus === 'saved' || showSavedBriefly) && ( )} {saveStatus === 'saving' ? 'Saving…' : saveStatus === 'unsaved' ? 'Unsaved changes' : 'All changes saved'} ), [recollectionName, saveStatus, showSavedBriefly] ) const titleNode = useMemo( () => ( {trigger} ), [trigger] ) useLayoutEffect(() => { setTitleContent(titleNode) return () => setTitleContent(null) }, [titleNode, setTitleContent]) return null }