feat: redesign navs

This commit is contained in:
2026-03-16 22:28:30 +01:00
parent 5bce586db6
commit 96cb3701ba
18 changed files with 501 additions and 432 deletions

View File

@@ -1,29 +1,23 @@
/**
* Logos page: BlockNote editor for the recollection. Content persisted in recollection store.
* Left sidebar for page/subpage hierarchy (one level). Layout and styling aligned with Flux.
* Logos page: BlockNote editor for the recollection. Content only; sidebar is at layout level.
* Content persisted in recollection store.
*/
import React, { useCallback, useEffect, useMemo, useRef, useState, forwardRef } from 'react'
import { useParams, useLocation, useNavigate } from 'react-router-dom'
import { useParams, useLocation, useSearchParams } from 'react-router-dom'
import { usePlatform } from '@/app/kosmos/KosmosContext'
import { FluxIcon } from '@/lib/icons'
import { useCreateBlockNote, getDefaultReactSlashMenuItems, SuggestionMenuController } from '@blocknote/react'
import { BlockNoteView } from '@blocknote/shadcn'
import { useTheme } from '@/lib/themeContext'
import { useRecollectionActions } from '../layout/RecollectionActionsContext'
import { useRecollectionSidebar } from '../layout/RecollectionSidebarContext'
import {
getLogosContent,
getLogosPageTree,
setLogosPageTree,
getLogosContentForPage,
setLogosContentForPage,
removeLogosPageContent,
type StoredLogosContent,
type LogosPageMeta,
type LogosPageId,
} from '../state/recollectionStore'
import { logosSchema } from './logosSchema'
import { LogosSidebar } from './LogosSidebar'
import { KatalogosPage } from '../katalogos/KatalogosPage'
import { toast } from 'sonner'
@@ -60,77 +54,30 @@ function patchBlockNoteRefWarning() {
}
}
const MAIN_PAGE_ID: LogosPageId = 'main'
export function LogosPage() {
const { recollectionId } = useParams<{ recollectionId: string }>()
const { pathname } = useLocation()
const navigate = useNavigate()
const [searchParams, setSearchParams] = useSearchParams()
const { recollections } = usePlatform()
const { theme } = useTheme()
const { setLogosSlot } = useRecollectionActions()
const { tree, activePageId } = useRecollectionSidebar()
const recollection = recollectionId ? recollections.find((p) => p.id === recollectionId) : null
const [tree, setTree] = useState<LogosPageMeta[]>([])
const [activePageId, setActivePageId] = useState<LogosPageId | null>(null)
const [reloadKey, setReloadKey] = useState(0)
const isKatalogosView = pathname.endsWith('/logos/katalogos')
const baseLogosPath = recollectionId ? `/recollections/${recollectionId}/logos` : ''
const handleSelectPage = useCallback(
(id: LogosPageId) => {
setActivePageId(id)
if (recollectionId) navigate(`${baseLogosPath}`)
},
[recollectionId, baseLogosPath, navigate]
)
const handleSelectKatalogos = useCallback(() => {
if (recollectionId) navigate(`${baseLogosPath}/katalogos`)
}, [recollectionId, baseLogosPath, navigate])
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const [saveStatus, setSaveStatus] = useState<'saved' | 'unsaved' | 'saving'>('saved')
// Load page tree and run migration when empty (treat existing single doc as "Main" page).
const isKatalogosView = pathname.endsWith('/logos/katalogos')
// When on Logos (not Katalogos) with a selected page but no page param, sync URL so refresh keeps the page.
useEffect(() => {
if (!recollectionId) return
let t = getLogosPageTree(recollectionId)
if (t.length === 0) {
const legacy = getLogosContent(recollectionId)
const main: LogosPageMeta = { id: MAIN_PAGE_ID, title: 'Main', parentId: null, position: 0 }
setLogosContentForPage(recollectionId, MAIN_PAGE_ID, legacy ?? [])
setLogosPageTree(recollectionId, [main])
t = [main]
}
setTree(t)
setActivePageId((prev) => {
const firstId = t[0]?.id ?? null
if (prev != null && t.some((p) => p.id === prev)) return prev
return firstId
})
}, [recollectionId])
// Persist tree when sidebar changes it.
useEffect(() => {
if (!recollectionId || tree.length === 0) return
setLogosPageTree(recollectionId, tree)
}, [recollectionId, tree])
const handleTreeChange = useCallback((newTree: LogosPageMeta[]) => {
setTree(newTree)
}, [])
const handleDeletePage = useCallback(
(pageId: LogosPageId) => {
if (recollectionId) removeLogosPageContent(recollectionId, pageId)
},
[recollectionId]
)
if (isKatalogosView || !activePageId || searchParams.get('page') === activePageId) return
setSearchParams({ page: activePageId }, { replace: true })
}, [isKatalogosView, activePageId, searchParams, setSearchParams])
const initialContent = useMemo(() => {
if (!recollectionId || !activePageId) return undefined
const content = getLogosContentForPage(recollectionId, activePageId)
// BlockNote requires a non-empty array of blocks; use undefined for default empty doc.
if (!content || !Array.isArray(content) || content.length === 0) return undefined
return content
}, [recollectionId, activePageId, reloadKey])
@@ -226,7 +173,6 @@ export function LogosPage() {
[editor]
)
// Render paths (no hooks below this point).
if (!recollectionId) {
return (
<div className="flex h-full min-h-0 flex-1 flex-col items-center justify-center p-8 text-muted-foreground">
@@ -235,53 +181,33 @@ export function LogosPage() {
)
}
const sidebarProps = {
recollectionId: recollectionId!,
tree,
onTreeChange: handleTreeChange,
activePageId,
onSelectPage: handleSelectPage,
onDeletePage: handleDeletePage,
isKatalogosView,
onSelectKatalogos: handleSelectKatalogos,
}
if (isKatalogosView) {
return (
<div className="flex h-full min-h-0 flex-1 flex-row bg-background">
<LogosSidebar {...sidebarProps} />
<div className="flex min-w-0 flex-1 flex-col overflow-auto">
<KatalogosPage />
</div>
<div className="flex h-full min-h-0 flex-1 flex-col overflow-auto bg-background">
<KatalogosPage />
</div>
)
}
if (activePageId == null || !editor) {
return (
<div className="flex h-full min-h-0 flex-1 flex-col bg-background">
<LogosSidebar {...sidebarProps} />
<div className="flex flex-1 items-center justify-center text-sm text-muted-foreground">
{tree.length === 0 ? 'Loading…' : 'Select a page'}
</div>
<div className="flex h-full min-h-0 flex-1 flex-col items-center justify-center bg-background text-sm text-muted-foreground">
{tree.length === 0 ? 'Loading…' : 'Select a page'}
</div>
)
}
return (
<div className="flex h-full min-h-0 flex-1 flex-row bg-background">
<LogosSidebar {...sidebarProps} />
<div className="flex min-w-0 flex-1 flex-col overflow-auto">
<div className="mx-auto w-full max-w-3xl p-4">
<BlockNoteViewWrapper
editor={editor as any}
theme={theme}
className="min-h-full w-full"
slashMenu={false}
>
<SuggestionMenuController triggerCharacter="/" getItems={getSlashMenuItems} />
</BlockNoteViewWrapper>
</div>
<div className="flex h-full min-h-0 flex-1 flex-col overflow-auto bg-background">
<div className="mx-auto w-full max-w-3xl p-4">
<BlockNoteViewWrapper
editor={editor as any}
theme={theme}
className="min-h-full w-full"
slashMenu={false}
>
<SuggestionMenuController triggerCharacter="/" getItems={getSlashMenuItems} />
</BlockNoteViewWrapper>
</div>
</div>
)