feat: refactoeing
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
Reference in New Issue
Block a user