feat: logos and flux

This commit is contained in:
2026-03-15 14:43:31 +01:00
parent 04c039bdde
commit 449d2cb382
8 changed files with 285 additions and 160 deletions

View File

@@ -36,7 +36,8 @@ import { ContextMenu, ContextMenuTrigger } from '@/components/ui/context-menu'
import { CanvasContextMenuContent } from '@/app/canvas/CanvasContextMenuContent' import { CanvasContextMenuContent } from '@/app/canvas/CanvasContextMenuContent'
import { useCanvasConnectionPathFromStore } from '@/app/canvas/useCanvasConnectionPathFromStore' import { useCanvasConnectionPathFromStore } from '@/app/canvas/useCanvasConnectionPathFromStore'
import { dispatchCanvasCommand } from '@/app/canvas/canvasStore' import { dispatchCanvasCommand } from '@/app/canvas/canvasStore'
import { CanvasMenubar } from '@/app/canvas/CanvasMenubar' import { useRecollectionMenubar } from '@/app/recollections/RecollectionMenubarContext'
import { FluxMenubarContent } from '@/app/recollections/flux/FluxMenubarContent'
import { createContextualNode } from '@/app/canvas/ContextualZoomNode' import { createContextualNode } from '@/app/canvas/ContextualZoomNode'
import { ViewportDisplayProvider } from '@/app/canvas/ViewportDisplayContext' import { ViewportDisplayProvider } from '@/app/canvas/ViewportDisplayContext'
import { FlowKeyboardShortcuts } from '@/components/graph/FlowKeyboardShortcuts' import { FlowKeyboardShortcuts } from '@/components/graph/FlowKeyboardShortcuts'
@@ -427,6 +428,52 @@ export function CanvasPage({ recollectionId }: CanvasPageProps) {
flowActionsRef.current?.pasteAtViewportCenter?.() flowActionsRef.current?.pasteAtViewportCenter?.()
}, []) }, [])
const { setCustomContent } = useRecollectionMenubar()
useEffect(() => {
setCustomContent(
<FluxMenubarContent
onImport={handleImportRecollection}
onExport={handleExportRecollection}
onSave={
recollectionId
? () => {
save()
toast.success('Saved')
}
: undefined
}
canSave={Boolean(recollectionId)}
saveStatus={saveStatus}
undo={undo}
redo={redo}
canUndo={canUndo}
canRedo={canRedo}
onDuplicate={handleDuplicate}
onCopy={handleCopy}
onPaste={handlePaste}
canDuplicate={selectedNodes.length > 0}
canCopy={selectedNodes.length === 1}
onFitView={() => flowActionsRef.current?.fitView?.()}
/>
)
return () => setCustomContent(null)
}, [
setCustomContent,
recollectionId,
saveStatus,
canUndo,
canRedo,
selectedNodes.length,
save,
undo,
redo,
handleDuplicate,
handleCopy,
handlePaste,
handleImportRecollection,
handleExportRecollection,
])
const graphContextValue = useMemo( const graphContextValue = useMemo(
() => ({ setNodes, setEdges, graphRef, edges }), () => ({ setNodes, setEdges, graphRef, edges }),
[setNodes, setEdges, edges] [setNodes, setEdges, edges]
@@ -646,30 +693,6 @@ export function CanvasPage({ recollectionId }: CanvasPageProps) {
onChange={onImportFileChange} onChange={onImportFileChange}
aria-hidden aria-hidden
/> />
<CanvasMenubar
onImport={handleImportRecollection}
onExport={handleExportRecollection}
onSave={
recollectionId
? () => {
save()
toast.success('Saved')
}
: undefined
}
canSave={Boolean(recollectionId)}
saveStatus={saveStatus}
undo={undo}
redo={redo}
canUndo={canUndo}
canRedo={canRedo}
onDuplicate={handleDuplicate}
onCopy={handleCopy}
onPaste={handlePaste}
canDuplicate={selectedNodes.length > 0}
canCopy={selectedNodes.length === 1}
onFitView={() => flowActionsRef.current?.fitView?.()}
/>
<div className="flex-1 min-h-0 relative flex flex-col"> <div className="flex-1 min-h-0 relative flex flex-col">
<div className="flex-1 min-h-0 flex flex-col"> <div className="flex-1 min-h-0 flex flex-col">
<GraphContext.Provider value={graphContextValue}> <GraphContext.Provider value={graphContextValue}>

View File

@@ -0,0 +1,43 @@
/**
* Layout for a single recollection: shared menubar (with optional custom content from active page) and outlet.
*/
import React, { useEffect } from 'react'
import { Link, Outlet, useParams } from 'react-router-dom'
import { usePlatform } from '@/app/kosmos/KosmosContext'
import { RecollectionMenubarProvider } from './RecollectionMenubarContext'
import { RecollectionMenubar } from './RecollectionMenubar'
export function RecollectionLayout() {
const { recollectionId } = useParams<{ recollectionId: string }>()
const { recollections, recordRecollectionAccess } = usePlatform()
useEffect(() => {
if (recollectionId) recordRecollectionAccess(recollectionId)
}, [recollectionId, recordRecollectionAccess])
const recollection = recollectionId ? recollections.find((p) => p.id === recollectionId) : null
if (!recollectionId) return null
if (!recollection) {
return (
<div className="flex flex-1 flex-col items-center justify-center gap-4 p-8">
<p className="text-sm text-muted-foreground">Recollection not found.</p>
<Link to="/recollections" className="text-sm text-primary hover:underline">
Back to recollections
</Link>
</div>
)
}
return (
<RecollectionMenubarProvider>
<div className="flex min-h-0 flex-1 flex-col">
<RecollectionMenubar />
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
<Outlet />
</div>
</div>
</RecollectionMenubarProvider>
)
}

View File

@@ -0,0 +1,59 @@
/**
* Shared base menubar for a recollection: Back, optional custom content (from context), Logos/Flux switcher.
*/
import React from 'react'
import { Link, NavLink, useParams } from 'react-router-dom'
import { usePlatform } from '@/app/kosmos/KosmosContext'
import { ArrowLeft, BookOpen, Workflow } from 'lucide-react'
import { useRecollectionMenubar } from './RecollectionMenubarContext'
export function RecollectionMenubar() {
const { recollectionId } = useParams<{ recollectionId: string }>()
const { recollections } = usePlatform()
const { customContent } = useRecollectionMenubar()
const recollection = recollectionId ? recollections.find((p) => p.id === recollectionId) : null
const base = recollectionId ? `/recollections/${recollectionId}` : ''
const defaultMiddle = recollection ? (
<span className="truncate text-sm font-medium text-muted-foreground">{recollection.name}</span>
) : null
return (
<div className="flex h-9 w-full shrink-0 items-center gap-2 border-b border-border/40 bg-background px-2">
<Link
to="/recollections"
aria-label="Back to recollections"
className="flex shrink-0 items-center rounded-sm px-2 py-1 text-sm outline-none focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground"
>
<ArrowLeft className="size-4" />
</Link>
<div className="flex min-w-0 flex-1 items-center">
{customContent ?? defaultMiddle}
</div>
{base && (
<nav className="flex shrink-0 items-center gap-0.5" aria-label="Recollection pages">
<NavLink
to={`${base}/logos`}
end
className={({ isActive }) =>
`flex items-center gap-1.5 rounded-sm px-2.5 py-1 text-sm outline-none focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground ${isActive ? 'bg-accent font-medium text-foreground' : 'text-muted-foreground'}`
}
>
<BookOpen className="size-3.5" />
Logos
</NavLink>
<NavLink
to={`${base}/flux`}
className={({ isActive }) =>
`flex items-center gap-1.5 rounded-sm px-2.5 py-1 text-sm outline-none focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground ${isActive ? 'bg-accent font-medium text-foreground' : 'text-muted-foreground'}`
}
>
<Workflow className="size-3.5" />
Flux
</NavLink>
</nav>
)}
</div>
)
}

View File

@@ -0,0 +1,36 @@
/**
* Context for the recollection menubar: pages (Logos, Flux) can register custom content
* to show in the middle of the shared menubar.
*/
import React, { createContext, useCallback, useContext, useState } from 'react'
export type RecollectionMenubarContextValue = {
/** Content to show in the middle of the menubar (menus, title, etc.). Set to null to use default (recollection name only). */
customContent: React.ReactNode
setCustomContent: (content: React.ReactNode) => void
}
const RecollectionMenubarContext = createContext<RecollectionMenubarContextValue | null>(null)
export function RecollectionMenubarProvider({ children }: { children: React.ReactNode }) {
const [customContent, setCustomContentState] = useState<React.ReactNode>(null)
const setCustomContent = useCallback((content: React.ReactNode) => {
setCustomContentState(() => content)
}, [])
const value: RecollectionMenubarContextValue = React.useMemo(
() => ({ customContent, setCustomContent }),
[customContent, setCustomContent]
)
return (
<RecollectionMenubarContext.Provider value={value}>
{children}
</RecollectionMenubarContext.Provider>
)
}
export function useRecollectionMenubar(): RecollectionMenubarContextValue {
const ctx = useContext(RecollectionMenubarContext)
if (!ctx) throw new Error('useRecollectionMenubar must be used within RecollectionMenubarProvider')
return ctx
}

View File

@@ -1,9 +1,10 @@
/** /**
* Menubar for the canvas page: Recollection (Import/Export), Edit (Undo/Redo, Duplicate/Copy/Paste, Rename), View (Fit View, Minimap). * Flux-specific menubar content: Recollection/Edit/View menus and centered title with save status.
* Registered with the recollection menubar when the Flux page is active.
*/ */
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Link, useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { import {
Menubar, Menubar,
MenubarContent, MenubarContent,
@@ -14,17 +15,16 @@ import {
} from '@/components/ui/menubar' } from '@/components/ui/menubar'
import { Kbd, KbdGroup } from '@/components/ui/kbd' import { Kbd, KbdGroup } from '@/components/ui/kbd'
import { usePlatform } from '@/app/kosmos/KosmosContext' import { usePlatform } from '@/app/kosmos/KosmosContext'
import { ArrowLeft, CheckCircle2, CircleDot, ClipboardPaste, Copy, CopyPlus, Download, FolderOpen, Loader2, Pencil, Redo2, Save, Undo2 } from 'lucide-react' import { CheckCircle2, CircleDot, ClipboardPaste, Copy, CopyPlus, Download, FolderOpen, Loader2, Pencil, Redo2, Save, Undo2 } from 'lucide-react'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import type { SaveStatus } from '@/app/canvas/useCanvasGraph' import type { SaveStatus } from '@/app/canvas/useCanvasGraph'
export type CanvasMenubarProps = { export type FluxMenubarContentProps = {
onImport: () => void onImport: () => void
onExport: () => void onExport: () => void
onSave?: () => void onSave?: () => void
canSave?: boolean canSave?: boolean
/** Shown next to recollection title: unsaved | saving | saved */
saveStatus?: SaveStatus saveStatus?: SaveStatus
undo: () => void undo: () => void
redo: () => void redo: () => void
@@ -47,7 +47,7 @@ function matchKey(ev: KeyboardEvent, want: { key: string; shiftKey: boolean }) {
return ev.key.toLowerCase() === want.key && !!mod && !!ev.shiftKey === want.shiftKey return ev.key.toLowerCase() === want.key && !!mod && !!ev.shiftKey === want.shiftKey
} }
export function CanvasMenubar({ export function FluxMenubarContent({
onImport, onImport,
onExport, onExport,
onSave, onSave,
@@ -63,7 +63,7 @@ export function CanvasMenubar({
canDuplicate = false, canDuplicate = false,
canCopy = false, canCopy = false,
onFitView, onFitView,
}: CanvasMenubarProps) { }: FluxMenubarContentProps) {
const { recollectionId } = useParams<{ recollectionId: string }>() const { recollectionId } = useParams<{ recollectionId: string }>()
const { recollections, renameRecollection } = usePlatform() const { recollections, renameRecollection } = usePlatform()
const recollectionName = useMemo( const recollectionName = useMemo(
@@ -110,7 +110,6 @@ export function CanvasMenubar({
if (isRenamingRecollection) { if (isRenamingRecollection) {
setRenameValue(recollectionName ?? '') setRenameValue(recollectionName ?? '')
ignoreNextBlurRef.current = true ignoreNextBlurRef.current = true
// Delay focus so the Recollection dropdown can close first and not steal focus back (which would trigger blur)
const t = setTimeout(() => { const t = setTimeout(() => {
renameInputRef.current?.focus() renameInputRef.current?.focus()
renameInputRef.current?.select() renameInputRef.current?.select()
@@ -126,9 +125,7 @@ export function CanvasMenubar({
setIsRenamingRecollection(false) setIsRenamingRecollection(false)
}, [recollectionId, renameRecollection, renameValue]) }, [recollectionId, renameRecollection, renameValue])
const cancelRename = useCallback(() => { const cancelRename = useCallback(() => setIsRenamingRecollection(false), [])
setIsRenamingRecollection(false)
}, [])
const handleRenameBlur = useCallback(() => { const handleRenameBlur = useCallback(() => {
if (ignoreNextBlurRef.current) { if (ignoreNextBlurRef.current) {
@@ -169,19 +166,12 @@ export function CanvasMenubar({
}, [undo, redo, canUndo, canRedo, onSave, canSave]) }, [undo, redo, canUndo, canRedo, onSave, canSave])
return ( return (
<div className="relative flex h-9 w-full shrink-0 items-center border-b border-border/40 bg-background"> <div className="relative flex flex-1 min-w-0 items-center">
<Menubar className="flex-1 shrink-0 rounded-none border-0 border-b-0 bg-transparent p-0 shadow-none"> <Menubar className="flex-1 shrink-0 rounded-none border-0 border-b-0 bg-transparent p-0 shadow-none min-w-0">
<Link
to="/recollections"
aria-label="Back to recollections"
className="flex shrink-0 items-center rounded-sm px-2 py-1 ml-1 text-sm outline-none focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground"
>
<ArrowLeft className="size-4" />
</Link>
<MenubarMenu> <MenubarMenu>
<MenubarTrigger className="font-normal text-muted-foreground">Recollection</MenubarTrigger> <MenubarTrigger className="font-normal text-muted-foreground">Recollection</MenubarTrigger>
<MenubarContent> <MenubarContent>
{recollectionId && ( {recollectionId && (
<> <>
{onSave != null && ( {onSave != null && (
<> <>
@@ -189,18 +179,13 @@ export function CanvasMenubar({
<Save className="h-4 w-4" /> <Save className="h-4 w-4" />
Save Save
<span className="ml-auto pl-4"> <span className="ml-auto pl-4">
<KbdGroup> <KbdGroup><Kbd>S</Kbd></KbdGroup>
<Kbd>S</Kbd>
</KbdGroup>
</span> </span>
</MenubarItem> </MenubarItem>
<MenubarSeparator /> <MenubarSeparator />
</> </>
)} )}
<MenubarItem <MenubarItem onClick={() => setIsRenamingRecollection(true)} className="gap-2">
onClick={() => setIsRenamingRecollection(true)}
className="gap-2"
>
<Pencil className="h-4 w-4" /> <Pencil className="h-4 w-4" />
Rename Rename
</MenubarItem> </MenubarItem>
@@ -223,53 +208,33 @@ export function CanvasMenubar({
<MenubarItem onClick={undo} disabled={!canUndo} className="gap-2"> <MenubarItem onClick={undo} disabled={!canUndo} className="gap-2">
<Undo2 className="h-4 w-4" /> <Undo2 className="h-4 w-4" />
Undo Undo
<span className="ml-auto pl-4"> <span className="ml-auto pl-4"><KbdGroup><Kbd> + Z</Kbd></KbdGroup></span>
<KbdGroup>
<Kbd> + Z</Kbd>
</KbdGroup>
</span>
</MenubarItem> </MenubarItem>
<MenubarItem onClick={redo} disabled={!canRedo} className="gap-2"> <MenubarItem onClick={redo} disabled={!canRedo} className="gap-2">
<Redo2 className="h-4 w-4" /> <Redo2 className="h-4 w-4" />
Redo Redo
<span className="ml-auto pl-4"> <span className="ml-auto pl-4"><KbdGroup><Kbd> + + Z</Kbd></KbdGroup></span>
<KbdGroup>
<Kbd> + + Z</Kbd>
</KbdGroup>
</span>
</MenubarItem> </MenubarItem>
{(onDuplicate != null || onCopy != null || onPaste != null) && <MenubarSeparator />} {(onDuplicate != null || onCopy != null || onPaste != null) && <MenubarSeparator />}
{onDuplicate != null && ( {onDuplicate != null && (
<MenubarItem onClick={onDuplicate} disabled={!canDuplicate} className="gap-2"> <MenubarItem onClick={onDuplicate} disabled={!canDuplicate} className="gap-2">
<CopyPlus className="h-4 w-4" /> <CopyPlus className="h-4 w-4" />
Duplicate Duplicate
<span className="ml-auto pl-4"> <span className="ml-auto pl-4"><KbdGroup><Kbd>D</Kbd></KbdGroup></span>
<KbdGroup>
<Kbd>D</Kbd>
</KbdGroup>
</span>
</MenubarItem> </MenubarItem>
)} )}
{onCopy != null && ( {onCopy != null && (
<MenubarItem onClick={onCopy} disabled={!canCopy} className="gap-2"> <MenubarItem onClick={onCopy} disabled={!canCopy} className="gap-2">
<Copy className="h-4 w-4" /> <Copy className="h-4 w-4" />
Copy Copy
<span className="ml-auto pl-4"> <span className="ml-auto pl-4"><KbdGroup><Kbd>C</Kbd></KbdGroup></span>
<KbdGroup>
<Kbd>C</Kbd>
</KbdGroup>
</span>
</MenubarItem> </MenubarItem>
)} )}
{onPaste != null && ( {onPaste != null && (
<MenubarItem onClick={onPaste} className="gap-2"> <MenubarItem onClick={onPaste} className="gap-2">
<ClipboardPaste className="h-4 w-4" /> <ClipboardPaste className="h-4 w-4" />
Paste Paste
<span className="ml-auto pl-4"> <span className="ml-auto pl-4"><KbdGroup><Kbd>V</Kbd></KbdGroup></span>
<KbdGroup>
<Kbd>V</Kbd>
</KbdGroup>
</span>
</MenubarItem> </MenubarItem>
)} )}
</MenubarContent> </MenubarContent>
@@ -280,78 +245,59 @@ export function CanvasMenubar({
{onFitView && ( {onFitView && (
<MenubarItem onClick={onFitView} className="gap-2"> <MenubarItem onClick={onFitView} className="gap-2">
Fit View Fit View
<span className="ml-auto pl-4"> <span className="ml-auto pl-4"><KbdGroup><Kbd>0</Kbd></KbdGroup></span>
<KbdGroup>
<Kbd>0</Kbd>
</KbdGroup>
</span>
</MenubarItem> </MenubarItem>
)} )}
</MenubarContent> </MenubarContent>
</MenubarMenu> </MenubarMenu>
</Menubar> </Menubar>
{recollectionId && ( {recollectionId && (
<div className="absolute left-1/2 -translate-x-1/2 flex items-center justify-center gap-2 max-w-[50%] min-w-[120px]"> <div className="absolute left-1/2 top-0 flex h-full -translate-x-1/2 items-center justify-center gap-2 max-w-[50%] min-w-[120px] pointer-events-none">
{isRenamingRecollection ? ( <div className="pointer-events-auto flex items-center justify-center gap-2">
<Input {isRenamingRecollection ? (
ref={renameInputRef} <Input
type="text" ref={renameInputRef}
value={renameValue} type="text"
onChange={(e) => setRenameValue(e.target.value)} value={renameValue}
onKeyDown={(e) => { onChange={(e) => setRenameValue(e.target.value)}
if (e.key === 'Enter') { onKeyDown={(e) => {
e.preventDefault() if (e.key === 'Enter') { e.preventDefault(); applyRename() }
applyRename() else if (e.key === 'Escape') { e.preventDefault(); cancelRename() }
} else if (e.key === 'Escape') { }}
e.preventDefault() onBlur={handleRenameBlur}
cancelRename() className="h-7 text-sm font-medium text-center font-serif"
} aria-label="Recollection name"
}} />
onBlur={handleRenameBlur} ) : (
className="h-7 text-sm font-medium text-center font-serif" <>
aria-label="Recollection name" <span className="truncate text-sm font-medium text-foreground font-serif">
/> {recollectionName ?? 'Untitled'}
) : ( </span>
<> <TooltipProvider delayDuration={300}>
<span className="pointer-events-none truncate text-sm font-medium text-foreground font-serif"> <Tooltip>
{recollectionName ?? 'Untitled'} <TooltipTrigger asChild>
</span> <span
<TooltipProvider delayDuration={300}> className="shrink-0 flex items-center text-muted-foreground cursor-default"
<Tooltip> aria-live="polite"
<TooltipTrigger asChild> aria-label={
<span saveStatus === 'saving' ? 'Saving' : saveStatus === 'unsaved' ? 'Unsaved changes' : 'All changes saved'
className="shrink-0 flex items-center text-muted-foreground cursor-default" }
aria-live="polite" >
aria-label={ {saveStatus === 'saving' && <Loader2 className="size-3.5 animate-spin" aria-hidden />}
saveStatus === 'saving' {saveStatus === 'unsaved' && <CircleDot className="size-3.5" aria-hidden />}
? 'Saving' {saveStatus !== 'unsaved' && (saveStatus === 'saved' || showSavedBriefly) && (
: saveStatus === 'unsaved' <CheckCircle2 className="size-3.5 text-muted-foreground/70" aria-hidden />
? 'Unsaved changes' )}
: 'All changes saved' </span>
} </TooltipTrigger>
> <TooltipContent side="bottom">
{saveStatus === 'saving' && ( {saveStatus === 'saving' ? 'Saving…' : saveStatus === 'unsaved' ? 'Unsaved changes' : 'All changes saved'}
<Loader2 className="size-3.5 animate-spin" aria-hidden /> </TooltipContent>
)} </Tooltip>
{saveStatus === 'unsaved' && ( </TooltipProvider>
<CircleDot className="size-3.5" aria-hidden /> </>
)} )}
{saveStatus !== 'unsaved' && (saveStatus === 'saved' || showSavedBriefly) && ( </div>
<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>
</>
)}
</div> </div>
)} )}
</div> </div>

View File

@@ -1,35 +1,26 @@
/** /**
* Route wrapper for the canvas: resolves recollectionId from URL and updates lastEditedAt on open. * Flux route: renders the graph canvas (CanvasPage) for the current recollection.
*/ */
import React, { useEffect, useRef } from 'react' import React, { useEffect, useRef } from 'react'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { CanvasPage } from './CanvasPage' import { CanvasPage } from '@/app/canvas/CanvasPage'
import { usePlatform } from '@/app/kosmos/KosmosContext' import { usePlatform } from '@/app/kosmos/KosmosContext'
export function CanvasRoute() { export function FluxRoute() {
const { recollectionId } = useParams<{ recollectionId: string }>() const { recollectionId } = useParams<{ recollectionId: string }>()
const { recollections, updateLastEdited } = usePlatform() const { updateLastEdited } = usePlatform()
const updateLastEditedRef = useRef(updateLastEdited) const updateLastEditedRef = useRef(updateLastEdited)
updateLastEditedRef.current = updateLastEdited updateLastEditedRef.current = updateLastEdited
const recollection = recollections.find((p) => p.id === recollectionId)
useEffect(() => { useEffect(() => {
if (recollectionId) updateLastEditedRef.current(recollectionId) if (recollectionId) updateLastEditedRef.current(recollectionId)
}, [recollectionId]) }, [recollectionId])
if (!recollectionId) return null if (!recollectionId) return null
if (!recollection) {
return (
<div className="flex flex-1 flex-col items-center justify-center gap-4 p-8">
<p className="text-sm text-muted-foreground">Recollection not found.</p>
</div>
)
}
return ( return (
<div className="flex min-h-0 flex-1 flex-col"> <div className="flex h-full min-h-0 flex-1 flex-col overflow-hidden">
<CanvasPage key={recollectionId} recollectionId={recollectionId} /> <CanvasPage key={recollectionId} recollectionId={recollectionId} />
</div> </div>
) )

View File

@@ -0,0 +1,21 @@
/**
* Logos page: default view for a recollection. Placeholder for now.
*/
import React, { useEffect } from 'react'
import { useRecollectionMenubar } from '../RecollectionMenubarContext'
export function LogosPage() {
const { setCustomContent } = useRecollectionMenubar()
useEffect(() => {
setCustomContent(null)
return () => setCustomContent(null)
}, [setCustomContent])
return (
<div className="flex h-full min-h-0 flex-1 flex-col items-center justify-center p-8 text-muted-foreground">
<p className="text-sm">Logos</p>
<p className="mt-1 text-xs">This page is empty for now.</p>
</div>
)
}

View File

@@ -7,7 +7,9 @@ import { registerBuiltinNodes } from './lib/graph/registerBuiltinNodes'
import { registerBuiltinConfigTypes } from './lib/graph/configTypes' import { registerBuiltinConfigTypes } from './lib/graph/configTypes'
import { KosmosPage } from './app/kosmos/KosmosPage' import { KosmosPage } from './app/kosmos/KosmosPage'
import { RecollectionsPage } from './app/recollections/RecollectionsPage' import { RecollectionsPage } from './app/recollections/RecollectionsPage'
import { CanvasRoute } from './app/canvas/CanvasRoute' import { RecollectionLayout } from './app/recollections/RecollectionLayout'
import { LogosPage } from './app/recollections/logos/LogosPage'
import { FluxRoute } from './app/recollections/flux/FluxRoute'
import './lib/prismSetup' import './lib/prismSetup'
import 'prismjs/themes/prism.css' import 'prismjs/themes/prism.css'
import './styles.css' import './styles.css'
@@ -24,7 +26,11 @@ createRoot(document.getElementById('root')!).render(
<Route path="/" element={<KosmosPage />}> <Route path="/" element={<KosmosPage />}>
<Route index element={<Navigate to="/recollections" replace />} /> <Route index element={<Navigate to="/recollections" replace />} />
<Route path="recollections" element={<RecollectionsPage />} /> <Route path="recollections" element={<RecollectionsPage />} />
<Route path="recollections/:recollectionId" element={<CanvasRoute />} /> <Route path="recollections/:recollectionId" element={<RecollectionLayout />}>
<Route index element={<Navigate to="logos" replace />} />
<Route path="logos" element={<LogosPage />} />
<Route path="flux" element={<FluxRoute />} />
</Route>
</Route> </Route>
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>