feat: logos and flux
This commit is contained in:
@@ -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}>
|
||||||
|
|||||||
43
frontend/src/app/recollections/RecollectionLayout.tsx
Normal file
43
frontend/src/app/recollections/RecollectionLayout.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
59
frontend/src/app/recollections/RecollectionMenubar.tsx
Normal file
59
frontend/src/app/recollections/RecollectionMenubar.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
)
|
)
|
||||||
21
frontend/src/app/recollections/logos/LogosPage.tsx
Normal file
21
frontend/src/app/recollections/logos/LogosPage.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user