diff --git a/frontend/src/app/canvas/CanvasPage.tsx b/frontend/src/app/canvas/CanvasPage.tsx index 10af3f3..3f08f51 100644 --- a/frontend/src/app/canvas/CanvasPage.tsx +++ b/frontend/src/app/canvas/CanvasPage.tsx @@ -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 } diff --git a/frontend/src/app/canvas/canvasGraphUtils.ts b/frontend/src/app/canvas/canvasGraphUtils.ts index 010a7df..9550fc6 100644 --- a/frontend/src/app/canvas/canvasGraphUtils.ts +++ b/frontend/src/app/canvas/canvasGraphUtils.ts @@ -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( diff --git a/frontend/src/app/canvas/useCanvasGraph.ts b/frontend/src/app/canvas/useCanvasGraph.ts index b90b4f5..845263f 100644 --- a/frontend/src/app/canvas/useCanvasGraph.ts +++ b/frontend/src/app/canvas/useCanvasGraph.ts @@ -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' diff --git a/frontend/src/app/kosmos/KosmosContext.tsx b/frontend/src/app/kosmos/KosmosContext.tsx index fb3603b..ae0760c 100644 --- a/frontend/src/app/kosmos/KosmosContext.tsx +++ b/frontend/src/app/kosmos/KosmosContext.tsx @@ -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' diff --git a/frontend/src/app/recollections/RecollectionLayout.tsx b/frontend/src/app/recollections/RecollectionLayout.tsx index ca0868c..3161975 100644 --- a/frontend/src/app/recollections/RecollectionLayout.tsx +++ b/frontend/src/app/recollections/RecollectionLayout.tsx @@ -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 }>() diff --git a/frontend/src/app/recollections/RecollectionsPage.tsx b/frontend/src/app/recollections/RecollectionsPage.tsx index 0a7f5be..10dfaad 100644 --- a/frontend/src/app/recollections/RecollectionsPage.tsx +++ b/frontend/src/app/recollections/RecollectionsPage.tsx @@ -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 diff --git a/frontend/src/app/recollections/RecollectionActionsContext.tsx b/frontend/src/app/recollections/layout/RecollectionActionsContext.tsx similarity index 99% rename from frontend/src/app/recollections/RecollectionActionsContext.tsx rename to frontend/src/app/recollections/layout/RecollectionActionsContext.tsx index 8137ae6..aeda717 100644 --- a/frontend/src/app/recollections/RecollectionActionsContext.tsx +++ b/frontend/src/app/recollections/layout/RecollectionActionsContext.tsx @@ -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' diff --git a/frontend/src/app/recollections/RecollectionEditViewMenus.tsx b/frontend/src/app/recollections/layout/RecollectionEditViewMenus.tsx similarity index 100% rename from frontend/src/app/recollections/RecollectionEditViewMenus.tsx rename to frontend/src/app/recollections/layout/RecollectionEditViewMenus.tsx diff --git a/frontend/src/app/recollections/layout/RecollectionFileMenu.tsx b/frontend/src/app/recollections/layout/RecollectionFileMenu.tsx new file mode 100644 index 0000000..9e77f2b --- /dev/null +++ b/frontend/src/app/recollections/layout/RecollectionFileMenu.tsx @@ -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 && ( + <> + + + Save + + + ⌘S + + + + + + )} + {recollectionId && ( + <> + setRenameModalOpen(true)} className="gap-2"> + + Rename + + + + )} + + + Import… + + + + Export… + + + ), + [recollectionId, onSave, canSave, onImport, onExport] + ) + + return ( + <> + + + File + + + + {menuItems} + + + + { + renameRecollection(id, newName) + setRenameModalOpen(false) + }} + /> + + ) +} + diff --git a/frontend/src/app/recollections/RecollectionMenubar.tsx b/frontend/src/app/recollections/layout/RecollectionMenubar.tsx similarity index 53% rename from frontend/src/app/recollections/RecollectionMenubar.tsx rename to frontend/src/app/recollections/layout/RecollectionMenubar.tsx index 74ecb71..29a1f50 100644 --- a/frontend/src/app/recollections/RecollectionMenubar.tsx +++ b/frontend/src/app/recollections/layout/RecollectionMenubar.tsx @@ -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 ( -
- - - -
- {titleContent ?? defaultTitle} +
+
+ + + +
+ {titleContent ?? defaultTitle} +
+
-
+
+
-
) } diff --git a/frontend/src/app/recollections/RecollectionMenubarContext.tsx b/frontend/src/app/recollections/layout/RecollectionMenubarContext.tsx similarity index 100% rename from frontend/src/app/recollections/RecollectionMenubarContext.tsx rename to frontend/src/app/recollections/layout/RecollectionMenubarContext.tsx diff --git a/frontend/src/app/recollections/RecollectionTitleContent.tsx b/frontend/src/app/recollections/layout/RecollectionTitleContent.tsx similarity index 60% rename from frontend/src/app/recollections/RecollectionTitleContent.tsx rename to frontend/src/app/recollections/layout/RecollectionTitleContent.tsx index ba2ab64..1c69be4 100644 --- a/frontend/src/app/recollections/RecollectionTitleContent.tsx +++ b/frontend/src/app/recollections/layout/RecollectionTitleContent.tsx @@ -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 | null>(null) const prevSaveStatusRef = useRef('saved') @@ -116,72 +105,19 @@ export function RecollectionTitleContent() { [recollectionName, saveStatus, showSavedBriefly] ) - const dropdownContent = useMemo(() => ( - <> - {recollectionId && ( - <> - {onSave != null && ( - <> - - - Save - ⌘S - - - - )} - setRenameModalOpen(true)} className="gap-2"> - - Rename - - - - )} - - - Import… - - - - Export… - - - ), [recollectionId, onSave, canSave, onImport, onExport]) - - const titleNode = useMemo(() => ( - - + const titleNode = useMemo( + () => ( + {trigger} - - - - - {dropdownContent} - - - - ), [trigger, dropdownContent]) + + ), + [trigger] + ) useLayoutEffect(() => { setTitleContent(titleNode) return () => setTitleContent(null) }, [titleNode, setTitleContent]) - return ( - { - renameRecollection(id, newName) - setRenameModalOpen(false) - }} - /> - ) + return null } diff --git a/frontend/src/app/recollections/RecollectionViewSwitcher.tsx b/frontend/src/app/recollections/layout/RecollectionViewSwitcher.tsx similarity index 100% rename from frontend/src/app/recollections/RecollectionViewSwitcher.tsx rename to frontend/src/app/recollections/layout/RecollectionViewSwitcher.tsx diff --git a/frontend/src/app/recollections/RecollectionsPageBackground.tsx b/frontend/src/app/recollections/layout/RecollectionsPageBackground.tsx similarity index 100% rename from frontend/src/app/recollections/RecollectionsPageBackground.tsx rename to frontend/src/app/recollections/layout/RecollectionsPageBackground.tsx diff --git a/frontend/src/app/recollections/RenameRecollectionDialog.tsx b/frontend/src/app/recollections/layout/RenameRecollectionDialog.tsx similarity index 100% rename from frontend/src/app/recollections/RenameRecollectionDialog.tsx rename to frontend/src/app/recollections/layout/RenameRecollectionDialog.tsx diff --git a/frontend/src/app/recollections/logos/LogosPage.tsx b/frontend/src/app/recollections/logos/LogosPage.tsx index 8ff37ea..caec149 100644 --- a/frontend/src/app/recollections/logos/LogosPage.tsx +++ b/frontend/src/app/recollections/logos/LogosPage.tsx @@ -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' diff --git a/frontend/src/app/recollections/logos/blocks/fluxOutputBlock.tsx b/frontend/src/app/recollections/logos/blocks/fluxOutputBlock.tsx index 946a722..b979968 100644 --- a/frontend/src/app/recollections/logos/blocks/fluxOutputBlock.tsx +++ b/frontend/src/app/recollections/logos/blocks/fluxOutputBlock.tsx @@ -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' diff --git a/frontend/src/app/recollections/recollectionGraphStorage.ts b/frontend/src/app/recollections/state/recollectionGraphStorage.ts similarity index 100% rename from frontend/src/app/recollections/recollectionGraphStorage.ts rename to frontend/src/app/recollections/state/recollectionGraphStorage.ts diff --git a/frontend/src/app/recollections/recollectionStore.ts b/frontend/src/app/recollections/state/recollectionStore.ts similarity index 100% rename from frontend/src/app/recollections/recollectionStore.ts rename to frontend/src/app/recollections/state/recollectionStore.ts diff --git a/frontend/src/components/nodes/render/useRenderingNodeState.ts b/frontend/src/components/nodes/render/useRenderingNodeState.ts index 0057a5d..b298d69 100644 --- a/frontend/src/components/nodes/render/useRenderingNodeState.ts +++ b/frontend/src/components/nodes/render/useRenderingNodeState.ts @@ -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 {