124 lines
4.6 KiB
TypeScript
124 lines
4.6 KiB
TypeScript
/**
|
|
* 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<ReturnType<typeof setTimeout> | null>(null)
|
|
const prevSaveStatusRef = useRef<string>('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(
|
|
() => (
|
|
<span className="flex items-center gap-1.5 truncate">
|
|
<span className="truncate text-sm font-medium font-serif max-w-[180px]">
|
|
{recollectionName ?? 'Untitled'}
|
|
</span>
|
|
<TooltipProvider delayDuration={300}>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<span
|
|
className="shrink-0 flex items-center text-muted-foreground cursor-default"
|
|
aria-live="polite"
|
|
aria-label={
|
|
saveStatus === 'saving' ? 'Saving' : saveStatus === 'unsaved' ? 'Unsaved changes' : 'All changes saved'
|
|
}
|
|
>
|
|
{saveStatus === 'saving' && <Loader2 className="size-3.5 animate-spin" aria-hidden />}
|
|
{saveStatus === 'unsaved' && <CircleDot className="size-3.5" aria-hidden />}
|
|
{saveStatus !== 'unsaved' && (saveStatus === 'saved' || showSavedBriefly) && (
|
|
<CheckCircle2 className="size-3.5 text-muted-foreground/70" aria-hidden />
|
|
)}
|
|
</span>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">
|
|
{saveStatus === 'saving' ? 'Saving…' : saveStatus === 'unsaved' ? 'Unsaved changes' : 'All changes saved'}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</span>
|
|
),
|
|
[recollectionName, saveStatus, showSavedBriefly]
|
|
)
|
|
|
|
const titleNode = useMemo(
|
|
() => (
|
|
<span className="flex items-center gap-1.5 rounded-sm px-2 py-1 text-sm">
|
|
{trigger}
|
|
</span>
|
|
),
|
|
[trigger]
|
|
)
|
|
|
|
useLayoutEffect(() => {
|
|
setTitleContent(titleNode)
|
|
return () => setTitleContent(null)
|
|
}, [titleNode, setTitleContent])
|
|
|
|
return null
|
|
}
|