feat: refactoeing

This commit is contained in:
2026-03-16 09:51:45 +01:00
parent f6193bf180
commit 9f56b728c0
20 changed files with 149 additions and 107 deletions

View File

@@ -36,8 +36,8 @@ import { ContextMenu, ContextMenuTrigger } from '@/components/ui/context-menu'
import { CanvasContextMenuContent } from '@/app/canvas/CanvasContextMenuContent'
import { useCanvasConnectionPathFromStore } from '@/app/canvas/useCanvasConnectionPathFromStore'
import { dispatchCanvasCommand } from '@/app/canvas/canvasStore'
import { useRecollectionActions } from '@/app/recollections/RecollectionActionsContext'
import type { StoredGraphState } from '@/app/recollections/recollectionStore'
import { useRecollectionActions } from '@/app/recollections/layout/RecollectionActionsContext'
import type { StoredGraphState } from '@/app/recollections/state/recollectionStore'
import { createContextualNode } from '@/app/canvas/ContextualZoomNode'
import { ViewportDisplayProvider } from '@/app/canvas/ViewportDisplayContext'
import { FlowKeyboardShortcuts } from '@/components/graph/FlowKeyboardShortcuts'
@@ -67,7 +67,7 @@ import {
} from '@/lib/graph/nodeRegistry'
import type { AppNode, AppEdge } from '@/lib/graph/nodeTypes'
import { toast } from 'sonner'
import { RECOLLECTION_VERSION } from '@/app/recollections/recollectionGraphStorage'
import { RECOLLECTION_VERSION } from '@/app/recollections/state/recollectionGraphStorage'
const SNAP_GRID: [number, number] = [15, 15]
const DUPLICATE_OFFSET = { x: 30, y: 30 }

View File

@@ -5,7 +5,7 @@
import type { AppNode, AppEdge } from '@/lib/graph/nodeTypes'
import { DEFAULT_NODE_STYLE } from '@/lib/graph/flowUtils'
import { loadGraphFromStorage } from '@/app/recollections/recollectionGraphStorage'
import { loadGraphFromStorage } from '@/app/recollections/state/recollectionGraphStorage'
/** Ensure each edge has data.targetType from the target node (for labels without depending on nodes in edgesForFlow). */
export function backfillEdgeTargetTypes(

View File

@@ -6,7 +6,7 @@
import { useCallback, useMemo, useRef, useState } from 'react'
import { useGraphStateWithHistory } from '@/hooks/useGraphStateWithHistory'
import { getInitialGraph } from '@/app/canvas/canvasGraphUtils'
import { saveGraphToStorage, RECOLLECTION_VERSION } from '@/app/recollections/recollectionGraphStorage'
import { saveGraphToStorage, RECOLLECTION_VERSION } from '@/app/recollections/state/recollectionGraphStorage'
import type { AppNode, AppEdge } from '@/lib/graph/nodeTypes'
export type SaveStatus = 'saved' | 'unsaved' | 'saving'

View File

@@ -5,7 +5,7 @@
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'
import type { Recollection } from './types'
import { saveGraphToStorage, RECOLLECTION_VERSION } from '@/app/recollections/recollectionGraphStorage'
import { saveGraphToStorage, RECOLLECTION_VERSION } from '@/app/recollections/state/recollectionGraphStorage'
const STORAGE_KEY = 'zui_platform_recollections'
const ORDER_STORAGE_KEY = 'zui_platform_recollection_order'

View File

@@ -5,10 +5,10 @@
import React, { useEffect } from 'react'
import { Link, Outlet, useParams } from 'react-router-dom'
import { usePlatform } from '@/app/kosmos/KosmosContext'
import { RecollectionMenubarProvider } from './RecollectionMenubarContext'
import { RecollectionActionsProvider } from './RecollectionActionsContext'
import { RecollectionTitleContent } from './RecollectionTitleContent'
import { RecollectionMenubar } from './RecollectionMenubar'
import { RecollectionMenubarProvider } from './layout/RecollectionMenubarContext'
import { RecollectionActionsProvider } from './layout/RecollectionActionsContext'
import { RecollectionTitleContent } from './layout/RecollectionTitleContent'
import { RecollectionMenubar } from './layout/RecollectionMenubar'
export function RecollectionLayout() {
const { recollectionId } = useParams<{ recollectionId: string }>()

View File

@@ -63,12 +63,12 @@ import {
removeGraphFromStorage,
RECOLLECTION_FILE_EXT,
RECOLLECTION_VERSION,
} from './recollectionGraphStorage'
import { getLogosContent, setLogosContent } from './recollectionStore'
} from './state/recollectionGraphStorage'
import { getLogosContent, setLogosContent } from './state/recollectionStore'
import { toast } from 'sonner'
import { NewRecollectionDialog } from '@/app/kosmos/NewRecollectionDialog'
import { RecollectionsPageBackground } from './RecollectionsPageBackground'
import { RenameRecollectionDialog } from './RenameRecollectionDialog'
import { RecollectionsPageBackground } from './layout/RecollectionsPageBackground'
import { RenameRecollectionDialog } from './layout/RenameRecollectionDialog'
import type { Recollection } from '@/app/kosmos/types'
const PAGE_SIZE_OPTIONS = [10, 25, 50] as const

View File

@@ -17,7 +17,7 @@ import {
type StoredGraphState,
type StoredLogosContent,
type RenderOutputCacheEntry,
} from './recollectionStore'
} from '../state/recollectionStore'
export type { RenderOutputCacheEntry }
import type { AppNode, AppEdge } from '@/lib/graph/nodeTypes'

View File

@@ -0,0 +1,102 @@
/**
* File menu for a recollection (emanation).
* Hosts Save, Rename, Import, Export that used to live under the title dropdown.
*/
import React, { useMemo, useState } from 'react'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Kbd, KbdGroup } from '@/components/ui/kbd'
import { useRecollectionActions } from './RecollectionActionsContext'
import { usePlatform } from '@/app/kosmos/KosmosContext'
import { RenameRecollectionDialog } from './RenameRecollectionDialog'
import { Download, FolderOpen, Pencil, Save } from 'lucide-react'
import { useParams } from 'react-router-dom'
export function RecollectionFileMenu() {
const { activeSlot, onImport, onExport } = useRecollectionActions()
const { recollections, renameRecollection } = usePlatform()
const { recollectionId } = useParams<{ recollectionId: string }>()
const [renameModalOpen, setRenameModalOpen] = useState(false)
const onSave = activeSlot?.onSave
const canSave = activeSlot?.canSave ?? false
const recollectionName = useMemo(
() => (recollectionId ? recollections.find((p) => p.id === recollectionId)?.name ?? '' : ''),
[recollectionId, recollections]
)
const menuItems = useMemo(
() => (
<>
{recollectionId && onSave != null && (
<>
<DropdownMenuItem onClick={onSave} disabled={!canSave} className="gap-2">
<Save className="h-4 w-4" />
Save
<span className="ml-auto pl-4">
<KbdGroup>
<Kbd>S</Kbd>
</KbdGroup>
</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
{recollectionId && (
<>
<DropdownMenuItem onClick={() => setRenameModalOpen(true)} className="gap-2">
<Pencil className="h-4 w-4" />
Rename
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem onClick={onImport} className="gap-2">
<FolderOpen className="h-4 w-4" />
Import
</DropdownMenuItem>
<DropdownMenuItem onClick={onExport} className="gap-2">
<Download className="h-4 w-4" />
Export
</DropdownMenuItem>
</>
),
[recollectionId, onSave, canSave, onImport, onExport]
)
return (
<>
<DropdownMenu>
<DropdownMenuTrigger className="rounded-sm px-2 py-1 text-sm font-normal text-muted-foreground outline-none focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground data-[state=open]:bg-accent">
File
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent align="start" className="z-[200] min-w-[12rem]">
{menuItems}
</DropdownMenuContent>
</DropdownMenuPortal>
</DropdownMenu>
<RenameRecollectionDialog
open={renameModalOpen}
onOpenChange={setRenameModalOpen}
recollectionId={recollectionId ?? ''}
initialName={recollectionName}
recollections={recollections}
onRename={(id, newName) => {
renameRecollection(id, newName)
setRenameModalOpen(false)
}}
/>
</>
)
}

View File

@@ -9,6 +9,7 @@ import { ArrowLeft } from 'lucide-react'
import { useRecollectionMenubar } from './RecollectionMenubarContext'
import { RecollectionEditViewMenus } from './RecollectionEditViewMenus'
import { RecollectionViewSwitcher } from './RecollectionViewSwitcher'
import { RecollectionFileMenu } from './RecollectionFileMenu'
export function RecollectionMenubar() {
const { recollectionId } = useParams<{ recollectionId: string }>()
@@ -21,21 +22,24 @@ export function RecollectionMenubar() {
) : null
return (
<div className="flex h-9 w-full shrink-0 items-center gap-2 overflow-visible 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 shrink-0 items-center gap-1.5">
{titleContent ?? defaultTitle}
<div className="flex h-9 w-full shrink-0 items-center justify-between gap-2 overflow-visible border-b border-border/40 bg-background px-2">
<div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden">
<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 shrink-0 items-center gap-1.5">
{titleContent ?? defaultTitle}
</div>
<RecollectionViewSwitcher />
</div>
<div className="flex min-w-0 flex-1 items-center gap-1 overflow-visible">
<div className="flex shrink-0 items-center gap-2 pl-2">
<RecollectionFileMenu />
<RecollectionEditViewMenus />
</div>
<RecollectionViewSwitcher />
</div>
)
}

View File

@@ -3,22 +3,12 @@
* the active slot (RecollectionActionsContext); Import/Export from layout-level handlers.
*/
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { useParams } from 'react-router-dom'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Kbd, KbdGroup } from '@/components/ui/kbd'
import { RenameRecollectionDialog } from './RenameRecollectionDialog'
import { usePlatform } from '@/app/kosmos/KosmosContext'
import { useRecollectionMenubar } from './RecollectionMenubarContext'
import { useRecollectionActions } from './RecollectionActionsContext'
import { CheckCircle2, ChevronDown, CircleDot, Download, FolderOpen, Loader2, Pencil, Save } from 'lucide-react'
import { CheckCircle2, CircleDot, Loader2 } from 'lucide-react'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
const SAVE_KEYS = { key: 's', shiftKey: false }
@@ -31,15 +21,14 @@ function matchKey(ev: KeyboardEvent, want: { key: string; shiftKey: boolean }) {
/** Registers the shared title + save dropdown as titleContent. Renders nothing. */
export function RecollectionTitleContent() {
const { setTitleContent } = useRecollectionMenubar()
const { activeSlot, onImport, onExport } = useRecollectionActions()
const { activeSlot } = useRecollectionActions()
const { recollectionId } = useParams<{ recollectionId: string }>()
const { recollections, renameRecollection } = usePlatform()
const { recollections } = usePlatform()
const recollectionName = useMemo(
() => (recollectionId ? recollections.find((p) => p.id === recollectionId)?.name ?? null : null),
[recollectionId, recollections]
)
const [renameModalOpen, setRenameModalOpen] = useState(false)
const [showSavedBriefly, setShowSavedBriefly] = useState(false)
const savedBrieflyTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const prevSaveStatusRef = useRef<string>('saved')
@@ -116,72 +105,19 @@ export function RecollectionTitleContent() {
[recollectionName, saveStatus, showSavedBriefly]
)
const dropdownContent = useMemo(() => (
<>
{recollectionId && (
<>
{onSave != null && (
<>
<DropdownMenuItem
onClick={onSave}
disabled={!canSave}
className="gap-2"
>
<Save className="h-4 w-4" />
Save
<span className="ml-auto pl-4"><KbdGroup><Kbd>S</Kbd></KbdGroup></span>
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem onClick={() => setRenameModalOpen(true)} className="gap-2">
<Pencil className="h-4 w-4" />
Rename
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem onClick={onImport} className="gap-2">
<FolderOpen className="h-4 w-4" />
Import
</DropdownMenuItem>
<DropdownMenuItem onClick={onExport} className="gap-2">
<Download className="h-4 w-4" />
Export
</DropdownMenuItem>
</>
), [recollectionId, onSave, canSave, onImport, onExport])
const titleNode = useMemo(() => (
<DropdownMenu>
<DropdownMenuTrigger className="flex items-center gap-1.5 rounded-sm px-2 py-1 text-sm outline-none focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground data-[state=open]:bg-accent">
const titleNode = useMemo(
() => (
<span className="flex items-center gap-1.5 rounded-sm px-2 py-1 text-sm">
{trigger}
<ChevronDown className="size-3.5 shrink-0 text-muted-foreground" />
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent align="start" className="z-[200] min-w-[12rem]">
{dropdownContent}
</DropdownMenuContent>
</DropdownMenuPortal>
</DropdownMenu>
), [trigger, dropdownContent])
</span>
),
[trigger]
)
useLayoutEffect(() => {
setTitleContent(titleNode)
return () => setTitleContent(null)
}, [titleNode, setTitleContent])
return (
<RenameRecollectionDialog
open={renameModalOpen}
onOpenChange={setRenameModalOpen}
recollectionId={recollectionId ?? ''}
initialName={recollectionName ?? ''}
recollections={recollections}
onRename={(id, newName) => {
renameRecollection(id, newName)
setRenameModalOpen(false)
}}
/>
)
return null
}

View File

@@ -10,8 +10,8 @@ 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 '../RecollectionActionsContext'
import { getLogosContent, setLogosContent, type StoredLogosContent } from '../recollectionStore'
import { useRecollectionActions } from '../layout/RecollectionActionsContext'
import { getLogosContent, setLogosContent, type StoredLogosContent } from '../state/recollectionStore'
import { logosSchema } from './logosSchema'
import { toast } from 'sonner'

View File

@@ -9,7 +9,7 @@ import React, { useCallback, useState } from 'react'
import { useParams } from 'react-router-dom'
import { createReactBlockSpec } from '@blocknote/react'
import type { ReactCustomBlockRenderProps } from '@blocknote/react'
import { getRenderOutputCache, type RenderOutputCacheEntry } from '../../recollectionStore'
import { getRenderOutputCache, type RenderOutputCacheEntry } from '../../state/recollectionStore'
import { Button } from '@/components/ui/button'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'

View File

@@ -9,7 +9,7 @@ import { useAbstractNode } from '@/lib/graph/abstractNode'
import { useSyncConnectionStatus } from '@/lib/graph/nodeLifecycle'
import { useCanvasStore, dispatchCanvasCommand } from '@/app/canvas/canvasStore'
import { usePlatform } from '@/app/kosmos/KosmosContext'
import { useOptionalRecollectionActions } from '@/app/recollections/RecollectionActionsContext'
import { useOptionalRecollectionActions } from '@/app/recollections/layout/RecollectionActionsContext'
import { getDefaultDataForType, getNextNodeId } from '@/lib/graph/flowUtils'
import { getDefaultStyle } from '@/lib/graph/nodeRegistry'
import {