From 04c039bdde799bf09685626737daebcf418e321a Mon Sep 17 00:00:00 2001 From: dtoro Date: Sun, 15 Mar 2026 14:11:11 +0100 Subject: [PATCH] freat: renaming --- frontend/docs/IMPROVEMENTS.md | 2 +- frontend/src/app/canvas/CanvasMenubar.tsx | 52 ++-- frontend/src/app/canvas/CanvasPage.tsx | 45 +-- frontend/src/app/canvas/CanvasRoute.tsx | 20 +- frontend/src/app/canvas/canvasGraphUtils.ts | 8 +- frontend/src/app/canvas/useCanvasGraph.ts | 18 +- frontend/src/app/keroma/KeromaPage.tsx | 20 -- frontend/src/app/kosmos/KosmosContext.tsx | 222 ++++++++------ frontend/src/app/kosmos/KosmosPage.tsx | 10 +- frontend/src/app/kosmos/KosmosSidebar.tsx | 74 ++--- ...ctDialog.tsx => NewRecollectionDialog.tsx} | 42 +-- frontend/src/app/kosmos/types.ts | 14 +- .../src/app/pleroma/projectGraphStorage.ts | 37 --- .../RecollectionsPage.tsx} | 290 +++++++++--------- .../RecollectionsPageBackground.tsx} | 4 +- .../recollections/recollectionGraphStorage.ts | 37 +++ frontend/src/lib/iconMap.tsx | 12 +- frontend/src/main.tsx | 10 +- 18 files changed, 455 insertions(+), 462 deletions(-) delete mode 100644 frontend/src/app/keroma/KeromaPage.tsx rename frontend/src/app/kosmos/{NewProjectDialog.tsx => NewRecollectionDialog.tsx} (70%) delete mode 100644 frontend/src/app/pleroma/projectGraphStorage.ts rename frontend/src/app/{pleroma/PleromaPage.tsx => recollections/RecollectionsPage.tsx} (80%) rename frontend/src/app/{pleroma/ProjectsPageBackground.tsx => recollections/RecollectionsPageBackground.tsx} (94%) create mode 100644 frontend/src/app/recollections/recollectionGraphStorage.ts diff --git a/frontend/docs/IMPROVEMENTS.md b/frontend/docs/IMPROVEMENTS.md index a6bc696..8142add 100644 --- a/frontend/docs/IMPROVEMENTS.md +++ b/frontend/docs/IMPROVEMENTS.md @@ -57,7 +57,7 @@ This doc summarizes recent improvements and suggested next steps for readability - **StoredGraphState** – shape for save/load (version + nodes + edges). - **FlowContext**: Module doc splits value into (1) Graph state, (2) Connection path state, (3) UI state. Same flat props, clearer sections. - **useGraphStateWithHistory**: JSDoc explains history (past/future), setNodes vs setNodesSilent, setStateImmediate. -- **projectGraphStorage**: Uses **StoredGraphState** from state.ts; re-exports type. Doc references state flow. +- **recollectionGraphStorage**: Uses **StoredGraphState** from state.ts; re-exports type. Doc references state flow. - **useRenderingNodeState**: Returns **displayStatus** (for NodeStatusIndicator/empty/error UI) and **lifecycle** (updating/error/paused for useSyncConnectionStatus). Hook calls useSyncConnectionStatus(id, state.lifecycle). Type includes RenderingNodeLifecycle. - **RenderingNode**: Uses **state.displayStatus** for NodeStatusIndicator instead of computing status locally. - **nodeLifecycle** and **connectionStatus**: Docs reference state.ts for overall state flow. diff --git a/frontend/src/app/canvas/CanvasMenubar.tsx b/frontend/src/app/canvas/CanvasMenubar.tsx index 28c6e4a..9f8b9c6 100644 --- a/frontend/src/app/canvas/CanvasMenubar.tsx +++ b/frontend/src/app/canvas/CanvasMenubar.tsx @@ -1,5 +1,5 @@ /** - * Menubar for the canvas page: Project (Import/Export), Edit (Undo/Redo, Duplicate/Copy/Paste, Rename), View (Fit View, Minimap). + * Menubar for the canvas page: Recollection (Import/Export), Edit (Undo/Redo, Duplicate/Copy/Paste, Rename), View (Fit View, Minimap). */ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -24,7 +24,7 @@ export type CanvasMenubarProps = { onExport: () => void onSave?: () => void canSave?: boolean - /** Shown next to project title: unsaved | saving | saved */ + /** Shown next to recollection title: unsaved | saving | saved */ saveStatus?: SaveStatus undo: () => void redo: () => void @@ -64,14 +64,14 @@ export function CanvasMenubar({ canCopy = false, onFitView, }: CanvasMenubarProps) { - const { projectId } = useParams<{ projectId: string }>() - const { projects, renameProject } = usePlatform() - const projectName = useMemo( - () => (projectId ? projects.find((p) => p.id === projectId)?.name ?? null : null), - [projectId, projects] + const { recollectionId } = useParams<{ recollectionId: string }>() + const { recollections, renameRecollection } = usePlatform() + const recollectionName = useMemo( + () => (recollectionId ? recollections.find((p) => p.id === recollectionId)?.name ?? null : null), + [recollectionId, recollections] ) - const [isRenamingProject, setIsRenamingProject] = useState(false) + const [isRenamingRecollection, setIsRenamingRecollection] = useState(false) const [renameValue, setRenameValue] = useState('') const [showSavedBriefly, setShowSavedBriefly] = useState(false) const renameInputRef = useRef(null) @@ -107,27 +107,27 @@ export function CanvasMenubar({ }, []) useEffect(() => { - if (isRenamingProject) { - setRenameValue(projectName ?? '') + if (isRenamingRecollection) { + setRenameValue(recollectionName ?? '') ignoreNextBlurRef.current = true - // Delay focus so the Project dropdown can close first and not steal focus back (which would trigger blur) + // Delay focus so the Recollection dropdown can close first and not steal focus back (which would trigger blur) const t = setTimeout(() => { renameInputRef.current?.focus() renameInputRef.current?.select() }, 100) return () => clearTimeout(t) } - }, [isRenamingProject, projectName]) + }, [isRenamingRecollection, recollectionName]) const applyRename = useCallback(() => { - if (!projectId || !renameProject) return + if (!recollectionId || !renameRecollection) return const trimmed = renameValue.trim() - if (trimmed) renameProject(projectId, trimmed) - setIsRenamingProject(false) - }, [projectId, renameProject, renameValue]) + if (trimmed) renameRecollection(recollectionId, trimmed) + setIsRenamingRecollection(false) + }, [recollectionId, renameRecollection, renameValue]) const cancelRename = useCallback(() => { - setIsRenamingProject(false) + setIsRenamingRecollection(false) }, []) const handleRenameBlur = useCallback(() => { @@ -172,16 +172,16 @@ export function CanvasMenubar({
- Project + Recollection - {projectId && ( + {recollectionId && ( <> {onSave != null && ( <> @@ -198,7 +198,7 @@ export function CanvasMenubar({ )} setIsRenamingProject(true)} + onClick={() => setIsRenamingRecollection(true)} className="gap-2" > @@ -290,9 +290,9 @@ export function CanvasMenubar({ - {projectId && ( + {recollectionId && (
- {isRenamingProject ? ( + {isRenamingRecollection ? ( ) : ( <> - {projectName ?? 'Untitled'} + {recollectionName ?? 'Untitled'} diff --git a/frontend/src/app/canvas/CanvasPage.tsx b/frontend/src/app/canvas/CanvasPage.tsx index 2cd8661..37875b6 100644 --- a/frontend/src/app/canvas/CanvasPage.tsx +++ b/frontend/src/app/canvas/CanvasPage.tsx @@ -1,6 +1,6 @@ /** * Canvas page: the graph editor (React Flow) with nodes, edges, context menu, import/export. - * Rendered inside the platform when a project is selected. + * Rendered inside the platform when a recollection is selected. */ import React, { useCallback, useEffect, useMemo, useRef } from 'react' @@ -66,7 +66,7 @@ import { } from '@/lib/graph/nodeRegistry' import type { AppNode, AppEdge } from '@/lib/graph/nodeTypes' import { toast } from 'sonner' -import { PROJECT_FILE_EXT, PROJECT_VERSION } from '@/app/pleroma/projectGraphStorage' +import { RECOLLECTION_FILE_EXT, RECOLLECTION_VERSION } from '@/app/recollections/recollectionGraphStorage' const SNAP_GRID: [number, number] = [15, 15] const DUPLICATE_OFFSET = { x: 30, y: 30 } @@ -149,11 +149,11 @@ function FullscreenNodeContent({ node }: { node: AppNode }) { } export type CanvasPageProps = { - /** Optional project id for future per-project graph loading */ - projectId?: string + /** Optional recollection id for per-recollection graph loading */ + recollectionId?: string } -export function CanvasPage({ projectId }: CanvasPageProps) { +export function CanvasPage({ recollectionId }: CanvasPageProps) { const { theme } = useTheme() const { showMinimap } = usePlatform() const { @@ -172,7 +172,7 @@ export function CanvasPage({ projectId }: CanvasPageProps) { setStateImmediate, save, saveStatus, - } = useCanvasGraph(projectId) + } = useCanvasGraph(recollectionId) const importInputRef = useRef(null) const [rfInstance, setRfInstance] = React.useState(null) @@ -323,19 +323,19 @@ export function CanvasPage({ projectId }: CanvasPageProps) { const onNodeDragStart = useCallback(() => saveForDragEnd(), [saveForDragEnd]) const onNodeDragStop = useCallback(() => commitDragEnd(), [commitDragEnd]) - const handleExportProject = useCallback(() => { - const state = { version: PROJECT_VERSION, nodes, edges } + const handleExportRecollection = useCallback(() => { + const state = { version: RECOLLECTION_VERSION, nodes, edges } const blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url - a.download = `project${PROJECT_FILE_EXT}` + a.download = `recollection${RECOLLECTION_FILE_EXT}` a.click() URL.revokeObjectURL(url) - toast.success('Project exported') + toast.success('Recollection exported') }, [nodes, edges]) - const handleImportProject = useCallback(() => importInputRef.current?.click(), []) + const handleImportRecollection = useCallback(() => importInputRef.current?.click(), []) const handleLoadExample = useCallback(() => { const { nodes: exampleNodes, edges: exampleEdges } = getExampleGraph() @@ -360,10 +360,10 @@ export function CanvasPage({ projectId }: CanvasPageProps) { const nodes = state.nodes as AppNode[] const edges = backfillEdgeTargetTypes(nodes, state.edges as AppEdge[]) setStateImmediate({ nodes, edges }) - if (state.version != null && state.version > PROJECT_VERSION) { - toast.error('Project was created with a newer app version') + if (state.version != null && state.version > RECOLLECTION_VERSION) { + toast.error('Recollection was created with a newer app version') } else { - toast.success('Project loaded') + toast.success('Recollection loaded') } } catch { toast.error('Invalid file: not valid JSON') @@ -534,8 +534,9 @@ export function CanvasPage({ projectId }: CanvasPageProps) { const clientX = click?.clientX ?? window.innerWidth / 2 const clientY = click?.clientY ?? window.innerHeight / 2 try { - const inst = rfInstance as { screenToFlowPosition?: (p: { x: number; y: number }) => { x: number; y: number }; project?: (p: { x: number; y: number }) => { x: number; y: number } } - const screenToFlow = inst.screenToFlowPosition ?? inst.project + type ScreenToFlow = (p: { x: number; y: number }) => { x: number; y: number } + const inst = rfInstance as { screenToFlowPosition?: ScreenToFlow; [k: string]: unknown } + const screenToFlow = inst.screenToFlowPosition ?? (inst['project'] as ScreenToFlow | undefined) const p = screenToFlow?.call(rfInstance, { x: clientX, y: clientY }) return p ? snapToGrid(p.x, p.y) : null } catch { @@ -646,17 +647,17 @@ export function CanvasPage({ projectId }: CanvasPageProps) { aria-hidden /> { save() toast.success('Saved') } : undefined } - canSave={Boolean(projectId)} + canSave={Boolean(recollectionId)} saveStatus={saveStatus} undo={undo} redo={redo} @@ -694,11 +695,11 @@ export function CanvasPage({ projectId }: CanvasPageProps) { Start adding a new node! Right‑click to add nodes.
- Import a project or paste a node. + Import a recollection or paste a node.
- diff --git a/frontend/src/app/canvas/CanvasRoute.tsx b/frontend/src/app/canvas/CanvasRoute.tsx index d77e783..71947b9 100644 --- a/frontend/src/app/canvas/CanvasRoute.tsx +++ b/frontend/src/app/canvas/CanvasRoute.tsx @@ -1,5 +1,5 @@ /** - * Route wrapper for the canvas: resolves projectId from URL and updates lastEditedAt on open. + * Route wrapper for the canvas: resolves recollectionId from URL and updates lastEditedAt on open. */ import React, { useEffect, useRef } from 'react' @@ -8,29 +8,29 @@ import { CanvasPage } from './CanvasPage' import { usePlatform } from '@/app/kosmos/KosmosContext' export function CanvasRoute() { - const { projectId } = useParams<{ projectId: string }>() - const { projects, updateLastEdited } = usePlatform() + const { recollectionId } = useParams<{ recollectionId: string }>() + const { recollections, updateLastEdited } = usePlatform() const updateLastEditedRef = useRef(updateLastEdited) updateLastEditedRef.current = updateLastEdited - const project = projects.find((p) => p.id === projectId) + const recollection = recollections.find((p) => p.id === recollectionId) useEffect(() => { - if (projectId) updateLastEditedRef.current(projectId) - }, [projectId]) + if (recollectionId) updateLastEditedRef.current(recollectionId) + }, [recollectionId]) - if (!projectId) return null - if (!project) { + if (!recollectionId) return null + if (!recollection) { return (
-

Project not found.

+

Recollection not found.

) } return (
- +
) } diff --git a/frontend/src/app/canvas/canvasGraphUtils.ts b/frontend/src/app/canvas/canvasGraphUtils.ts index d5ad5e6..010a7df 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/pleroma/projectGraphStorage' +import { loadGraphFromStorage } from '@/app/recollections/recollectionGraphStorage' /** Ensure each edge has data.targetType from the target node (for labels without depending on nodes in edgesForFlow). */ export function backfillEdgeTargetTypes( @@ -70,9 +70,9 @@ export function getExampleGraph(): { nodes: AppNode[]; edges: AppEdge[] } { return { nodes, edges } } -export function getInitialGraph(projectId: string | undefined): { nodes: AppNode[]; edges: AppEdge[] } { - if (projectId) { - const stored = loadGraphFromStorage(projectId) +export function getInitialGraph(recollectionId: string | undefined): { nodes: AppNode[]; edges: AppEdge[] } { + if (recollectionId) { + const stored = loadGraphFromStorage(recollectionId) if (stored && (stored.nodes.length > 0 || stored.edges.length > 0)) { const nodes = stored.nodes as AppNode[] const edges = backfillEdgeTargetTypes(nodes, stored.edges as AppEdge[]) diff --git a/frontend/src/app/canvas/useCanvasGraph.ts b/frontend/src/app/canvas/useCanvasGraph.ts index d3c89f3..b90b4f5 100644 --- a/frontend/src/app/canvas/useCanvasGraph.ts +++ b/frontend/src/app/canvas/useCanvasGraph.ts @@ -1,25 +1,25 @@ /** * Hook for canvas graph state and persistence. Wraps useGraphStateWithHistory - * with initial graph from project storage (or example). Save is explicit via save(). + * with initial graph from recollection storage (or example). Save is explicit via save(). */ import { useCallback, useMemo, useRef, useState } from 'react' import { useGraphStateWithHistory } from '@/hooks/useGraphStateWithHistory' import { getInitialGraph } from '@/app/canvas/canvasGraphUtils' -import { saveGraphToStorage, PROJECT_VERSION } from '@/app/pleroma/projectGraphStorage' +import { saveGraphToStorage, RECOLLECTION_VERSION } from '@/app/recollections/recollectionGraphStorage' import type { AppNode, AppEdge } from '@/lib/graph/nodeTypes' export type SaveStatus = 'saved' | 'unsaved' | 'saving' export type UseCanvasGraphResult = ReturnType & { - /** Persist current nodes/edges to storage. No-op when projectId is missing. */ + /** Persist current nodes/edges to storage. No-op when recollectionId is missing. */ save: () => void /** For menubar: show "Unsaved changes" | "Saving…" | "All changes saved". */ saveStatus: SaveStatus } -export function useCanvasGraph(projectId: string | undefined): UseCanvasGraphResult { - const initialGraph = useMemo(() => getInitialGraph(projectId), [projectId]) +export function useCanvasGraph(recollectionId: string | undefined): UseCanvasGraphResult { + const initialGraph = useMemo(() => getInitialGraph(recollectionId), [recollectionId]) const result = useGraphStateWithHistory(initialGraph.nodes, initialGraph.edges) const { nodes, edges } = result @@ -41,14 +41,14 @@ export function useCanvasGraph(projectId: string | undefined): UseCanvasGraphRes const saveStatus: SaveStatus = isSaving ? 'saving' : isDirty ? 'unsaved' : 'saved' const save = useCallback(() => { - if (!projectId) return + if (!recollectionId) return const snapshot = JSON.stringify({ nodes: nodesRef.current, edges: edgesRef.current, }) setIsSaving(true) - saveGraphToStorage(projectId, { - version: PROJECT_VERSION, + saveGraphToStorage(recollectionId, { + version: RECOLLECTION_VERSION, nodes: nodesRef.current, edges: edgesRef.current, }) @@ -57,7 +57,7 @@ export function useCanvasGraph(projectId: string | undefined): UseCanvasGraphRes setLastSavedSerialized(snapshot) setIsSaving(false) }, SAVING_DISPLAY_MS) - }, [projectId]) + }, [recollectionId]) return { ...result, save, saveStatus } } diff --git a/frontend/src/app/keroma/KeromaPage.tsx b/frontend/src/app/keroma/KeromaPage.tsx deleted file mode 100644 index 699812e..0000000 --- a/frontend/src/app/keroma/KeromaPage.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Keroma page. Rendered at /keroma. - */ - -import React from 'react' - -export function KeromaPage() { - return ( -
-
-

- Keroma -

-
-
-

Welcome to Keroma.

-
-
- ) -} diff --git a/frontend/src/app/kosmos/KosmosContext.tsx b/frontend/src/app/kosmos/KosmosContext.tsx index da1fd7e..fb3603b 100644 --- a/frontend/src/app/kosmos/KosmosContext.tsx +++ b/frontend/src/app/kosmos/KosmosContext.tsx @@ -1,16 +1,19 @@ /** - * Platform context: projects list and handlers for create/delete/rename/updateLastEdited/reorder/restore. - * Used by AppSidebar, ProjectsTablePage, and canvas route. + * Platform context: recollections list and handlers for create/delete/rename/updateLastEdited/reorder/restore. + * Used by AppSidebar, RecollectionsPage, and canvas route. */ import React, { createContext, useCallback, useContext, useMemo, useState } from 'react' -import type { Project } from './types' -import { saveGraphToStorage } from '@/app/pleroma/projectGraphStorage' -import { PROJECT_VERSION } from '@/app/pleroma/projectGraphStorage' +import type { Recollection } from './types' +import { saveGraphToStorage, RECOLLECTION_VERSION } from '@/app/recollections/recollectionGraphStorage' -const STORAGE_KEY = 'zui_platform_projects' -const ORDER_STORAGE_KEY = 'zui_platform_project_order' -const RECENT_STORAGE_KEY = 'zui_platform_recent_project_ids' +const STORAGE_KEY = 'zui_platform_recollections' +const ORDER_STORAGE_KEY = 'zui_platform_recollection_order' +const RECENT_STORAGE_KEY = 'zui_platform_recent_recollection_ids' + +const LEGACY_STORAGE_KEY = 'zui_platform_emanations' +const LEGACY_ORDER_STORAGE_KEY = 'zui_platform_emanation_order' +const LEGACY_RECENT_STORAGE_KEY = 'zui_platform_recent_emanation_ids' const CANVAS_MINIMAP_KEY = 'zui_canvas_show_minimap' const AI_CONNECTION_KEY = 'zui_ai_connection' const RECENT_MAX = 5 @@ -72,8 +75,15 @@ function saveShowMinimap(value: boolean) { function loadOrder(): string[] { try { - const raw = localStorage.getItem(ORDER_STORAGE_KEY) - if (!raw) return [] + let raw = localStorage.getItem(ORDER_STORAGE_KEY) + if (!raw) { + const legacy = localStorage.getItem(LEGACY_ORDER_STORAGE_KEY) + if (legacy) { + localStorage.setItem(ORDER_STORAGE_KEY, legacy) + localStorage.removeItem(LEGACY_ORDER_STORAGE_KEY) + raw = legacy + } else return [] + } const parsed = JSON.parse(raw) as unknown return Array.isArray(parsed) ? parsed.filter((id): id is string => typeof id === 'string') : [] } catch { @@ -87,8 +97,15 @@ function saveOrder(ids: string[]) { function loadRecentIds(): string[] { try { - const raw = localStorage.getItem(RECENT_STORAGE_KEY) - if (!raw) return [] + let raw = localStorage.getItem(RECENT_STORAGE_KEY) + if (!raw) { + const legacy = localStorage.getItem(LEGACY_RECENT_STORAGE_KEY) + if (legacy) { + localStorage.setItem(RECENT_STORAGE_KEY, legacy) + localStorage.removeItem(LEGACY_RECENT_STORAGE_KEY) + raw = legacy + } else return [] + } const parsed = JSON.parse(raw) as unknown return Array.isArray(parsed) ? parsed.filter((id): id is string => typeof id === 'string').slice(0, RECENT_MAX) : [] } catch { @@ -100,41 +117,48 @@ function saveRecentIds(ids: string[]) { localStorage.setItem(RECENT_STORAGE_KEY, JSON.stringify(ids)) } -function loadProjects(): Project[] { +function loadRecollections(): Recollection[] { try { - const raw = localStorage.getItem(STORAGE_KEY) - if (!raw) return [] + let raw = localStorage.getItem(STORAGE_KEY) + if (!raw) { + const legacy = localStorage.getItem(LEGACY_STORAGE_KEY) + if (legacy) { + localStorage.setItem(STORAGE_KEY, legacy) + localStorage.removeItem(LEGACY_STORAGE_KEY) + raw = legacy + } else return [] + } const parsed = JSON.parse(raw) as unknown if (!Array.isArray(parsed)) return [] return parsed .filter( - (p): p is Project => + (p): p is Recollection => p && typeof p === 'object' && - typeof (p as Project).id === 'string' && - typeof (p as Project).name === 'string' && - typeof (p as Project).iconId === 'string' && - typeof (p as Project).createdAt === 'number' + typeof (p as Recollection).id === 'string' && + typeof (p as Recollection).name === 'string' && + typeof (p as Recollection).iconId === 'string' && + typeof (p as Recollection).createdAt === 'number' ) .map((p) => ({ ...p, - lastEditedAt: typeof (p as Project).lastEditedAt === 'number' ? (p as Project).lastEditedAt : p.createdAt, + lastEditedAt: typeof (p as Recollection).lastEditedAt === 'number' ? (p as Recollection).lastEditedAt : p.createdAt, })) } catch { return [] } } -function saveProjects(projects: Project[]) { - localStorage.setItem(STORAGE_KEY, JSON.stringify(projects)) +function saveRecollections(recollections: Recollection[]) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(recollections)) } export type GraphSnapshot = { nodes: unknown[]; edges: unknown[] } -/** Projects sorted by projectOrder, then by lastEditedAt desc for any not in order */ -export function sortProjectsByOrder(projects: Project[], order: string[]): Project[] { - const byId = new Map(projects.map((p) => [p.id, p])) - const ordered: Project[] = [] +/** Recollections sorted by recollectionOrder, then by lastEditedAt desc for any not in order */ +export function sortRecollectionsByOrder(recollections: Recollection[], order: string[]): Recollection[] { + const byId = new Map(recollections.map((p) => [p.id, p])) + const ordered: Recollection[] = [] const seen = new Set() for (const id of order) { const p = byId.get(id) @@ -143,27 +167,27 @@ export function sortProjectsByOrder(projects: Project[], order: string[]): Proje seen.add(id) } } - const rest = projects + const rest = recollections .filter((p) => !seen.has(p.id)) .sort((a, b) => (b.lastEditedAt ?? b.createdAt) - (a.lastEditedAt ?? a.createdAt)) return [...ordered, ...rest] } export type KosmosContextValue = { - projects: Project[] - projectOrder: string[] - /** Projects in display order (sidebar order, then lastEditedAt) */ - orderedProjects: Project[] - /** Last RECENT_MAX accessed project IDs (most recent first) */ - recentProjectIds: string[] - recordProjectAccess: (id: string) => void - persist: (next: Project[]) => void - createProject: (project: Project) => void - deleteProject: (id: string) => void - renameProject: (id: string, name: string) => void + recollections: Recollection[] + recollectionOrder: string[] + /** Recollections in display order (sidebar order, then lastEditedAt) */ + orderedRecollections: Recollection[] + /** Last RECENT_MAX accessed recollection IDs (most recent first) */ + recentRecollectionIds: string[] + recordRecollectionAccess: (id: string) => void + persist: (next: Recollection[]) => void + createRecollection: (recollection: Recollection) => void + deleteRecollection: (id: string) => void + renameRecollection: (id: string, name: string) => void updateLastEdited: (id: string) => void - reorderProjects: (orderedIds: string[]) => void - restoreProject: (project: Project, graphSnapshot: GraphSnapshot | null) => void + reorderRecollections: (orderedIds: string[]) => void + restoreRecollection: (recollection: Recollection, graphSnapshot: GraphSnapshot | null) => void /** Canvas: show React Flow minimap (persisted) */ showMinimap: boolean setShowMinimap: (value: boolean) => void @@ -175,9 +199,9 @@ export type KosmosContextValue = { const KosmosContext = createContext(null) export function KosmosProvider({ children }: { children: React.ReactNode }) { - const [projects, setProjects] = useState(loadProjects) - const [projectOrder, setProjectOrder] = useState(loadOrder) - const [recentProjectIds, setRecentProjectIds] = useState(loadRecentIds) + const [recollections, setRecollections] = useState(loadRecollections) + const [recollectionOrder, setRecollectionOrder] = useState(loadOrder) + const [recentRecollectionIds, setRecentRecollectionIds] = useState(loadRecentIds) const [showMinimap, setShowMinimapState] = useState(loadShowMinimap) const [aiConnection, setAiConnectionState] = useState(loadAiConnection) @@ -191,128 +215,128 @@ export function KosmosProvider({ children }: { children: React.ReactNode }) { saveAiConnection(value) }, []) - const persist = useCallback((next: Project[]) => { - setProjects(next) - saveProjects(next) + const persist = useCallback((next: Recollection[]) => { + setRecollections(next) + saveRecollections(next) }, []) - const createProject = useCallback( - (project: Project) => { - const withEdited = { ...project, lastEditedAt: project.createdAt } - persist([...projects, withEdited]) - setProjectOrder((prev) => { - const next = prev.includes(project.id) ? prev : [project.id, ...prev] + const createRecollection = useCallback( + (recollection: Recollection) => { + const withEdited = { ...recollection, lastEditedAt: recollection.createdAt } + persist([...recollections, withEdited]) + setRecollectionOrder((prev) => { + const next = prev.includes(recollection.id) ? prev : [recollection.id, ...prev] saveOrder(next) return next }) }, - [projects, persist] + [recollections, persist] ) - const deleteProject = useCallback( + const deleteRecollection = useCallback( (id: string) => { - const next = projects.filter((p) => p.id !== id) + const next = recollections.filter((p) => p.id !== id) persist(next) - setProjectOrder((prev) => { + setRecollectionOrder((prev) => { const nextOrder = prev.filter((oid) => oid !== id) saveOrder(nextOrder) return nextOrder }) - setRecentProjectIds((prev) => { + setRecentRecollectionIds((prev) => { const next = prev.filter((oid) => oid !== id) saveRecentIds(next) return next }) }, - [projects, persist] + [recollections, persist] ) - const renameProject = useCallback( + const renameRecollection = useCallback( (id: string, name: string) => { - const next = projects.map((p) => (p.id === id ? { ...p, name } : p)) + const next = recollections.map((p) => (p.id === id ? { ...p, name } : p)) persist(next) }, - [projects, persist] + [recollections, persist] ) const updateLastEdited = useCallback( (id: string) => { const now = Date.now() - const next = projects.map((p) => (p.id === id ? { ...p, lastEditedAt: now } : p)) + const next = recollections.map((p) => (p.id === id ? { ...p, lastEditedAt: now } : p)) persist(next) }, - [projects, persist] + [recollections, persist] ) - const reorderProjects = useCallback((orderedIds: string[]) => { - setProjectOrder(orderedIds) + const reorderRecollections = useCallback((orderedIds: string[]) => { + setRecollectionOrder(orderedIds) saveOrder(orderedIds) }, []) - const recordProjectAccess = useCallback((id: string) => { - setRecentProjectIds((prev) => { + const recordRecollectionAccess = useCallback((id: string) => { + setRecentRecollectionIds((prev) => { const next = [id, ...prev.filter((x) => x !== id)].slice(0, RECENT_MAX) saveRecentIds(next) return next }) }, []) - const restoreProject = useCallback( - (project: Project, graphSnapshot: GraphSnapshot | null) => { - persist([...projects, project]) - setProjectOrder((prev) => { - const next = prev.includes(project.id) ? prev : [project.id, ...prev] + const restoreRecollection = useCallback( + (recollection: Recollection, graphSnapshot: GraphSnapshot | null) => { + persist([...recollections, recollection]) + setRecollectionOrder((prev) => { + const next = prev.includes(recollection.id) ? prev : [recollection.id, ...prev] saveOrder(next) return next }) if (graphSnapshot) { - saveGraphToStorage(project.id, { - version: PROJECT_VERSION, + saveGraphToStorage(recollection.id, { + version: RECOLLECTION_VERSION, nodes: graphSnapshot.nodes, edges: graphSnapshot.edges, }) } }, - [projects, persist] + [recollections, persist] ) - const orderedProjects = useMemo( - () => sortProjectsByOrder(projects, projectOrder), - [projects, projectOrder] + const orderedRecollections = useMemo( + () => sortRecollectionsByOrder(recollections, recollectionOrder), + [recollections, recollectionOrder] ) const value: KosmosContextValue = useMemo( () => ({ - projects, - projectOrder, - orderedProjects, - recentProjectIds, - recordProjectAccess, + recollections, + recollectionOrder, + orderedRecollections, + recentRecollectionIds, + recordRecollectionAccess, persist, - createProject, - deleteProject, - renameProject, + createRecollection, + deleteRecollection, + renameRecollection, updateLastEdited, - reorderProjects, - restoreProject, + reorderRecollections, + restoreRecollection, showMinimap, setShowMinimap, aiConnection, setAiConnection, }), [ - projects, - projectOrder, - orderedProjects, - recentProjectIds, - recordProjectAccess, + recollections, + recollectionOrder, + orderedRecollections, + recentRecollectionIds, + recordRecollectionAccess, persist, - createProject, - deleteProject, - renameProject, + createRecollection, + deleteRecollection, + renameRecollection, updateLastEdited, - reorderProjects, - restoreProject, + reorderRecollections, + restoreRecollection, showMinimap, setShowMinimap, aiConnection, diff --git a/frontend/src/app/kosmos/KosmosPage.tsx b/frontend/src/app/kosmos/KosmosPage.tsx index 0f7305b..4557870 100644 --- a/frontend/src/app/kosmos/KosmosPage.tsx +++ b/frontend/src/app/kosmos/KosmosPage.tsx @@ -1,5 +1,5 @@ /** - * Platform: main layout with sidebar and header. Renders child routes (projects list or canvas) via Outlet. + * Platform: main layout with sidebar and header. Renders child routes (recollections list or canvas) via Outlet. */ import React, { useEffect, useState } from 'react' @@ -9,13 +9,13 @@ import { KosmosProvider, usePlatform } from './KosmosContext' import { KosmosSidebar } from './KosmosSidebar' function KosmosLayoutInner() { - const { projectId } = useParams<{ projectId: string }>() - const { recordProjectAccess } = usePlatform() + const { recollectionId } = useParams<{ recollectionId: string }>() + const { recordRecollectionAccess } = usePlatform() const [sidebarOpen, setSidebarOpen] = useState(true) useEffect(() => { - if (projectId) recordProjectAccess(projectId) - }, [projectId, recordProjectAccess]) + if (recollectionId) recordRecollectionAccess(recollectionId) + }, [recollectionId, recordRecollectionAccess]) return ( diff --git a/frontend/src/app/kosmos/KosmosSidebar.tsx b/frontend/src/app/kosmos/KosmosSidebar.tsx index 05ffa80..d266faa 100644 --- a/frontend/src/app/kosmos/KosmosSidebar.tsx +++ b/frontend/src/app/kosmos/KosmosSidebar.tsx @@ -1,5 +1,5 @@ /** - * Platform sidebar: All projects, Recently used, New project. + * Platform sidebar: Recollections, Recently used, New recollection. */ import React, { useCallback, useMemo } from 'react' @@ -21,32 +21,30 @@ import { Plus, Settings, Triangle } from 'lucide-react' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { useSidebar } from '@/components/ui/sidebar' import { usePlatform } from './KosmosContext' -import { getProjectIcon } from '@/lib/iconMap' -import { NewProjectDialog } from './NewProjectDialog' +import { getRecollectionIcon } from '@/lib/iconMap' +import { NewRecollectionDialog } from './NewRecollectionDialog' import { SettingsDialog } from './SettingsDialog' -import type { Project } from './types' +import type { Recollection } from './types' export function KosmosSidebar() { const { state, setOpen } = useSidebar() const isCollapsed = state === 'collapsed' - const { orderedProjects, createProject, recentProjectIds } = usePlatform() - const recentProjects = useMemo(() => { - const byId = new Map(orderedProjects.map((p) => [p.id, p])) - return recentProjectIds.map((id) => byId.get(id)).filter((p): p is Project => p != null) - }, [orderedProjects, recentProjectIds]) + const { orderedRecollections, createRecollection, recentRecollectionIds } = usePlatform() + const recentRecollections = useMemo(() => { + const byId = new Map(orderedRecollections.map((p) => [p.id, p])) + return recentRecollectionIds.map((id) => byId.get(id)).filter((p): p is Recollection => p != null) + }, [orderedRecollections, recentRecollectionIds]) const navigate = useNavigate() const location = useLocation() - const { projectId: selectedProjectId } = useParams<{ projectId: string }>() - const isKeroma = location.pathname === '/keroma' + const { recollectionId: selectedRecollectionId } = useParams<{ recollectionId: string }>() + const handleSelectRecollection = useCallback((id: string) => navigate(`/recollections/${id}`), [navigate]) - const handleSelectProject = useCallback((id: string) => navigate(`/projects/${id}`), [navigate]) - - const handleCreateProject = useCallback( - (project: Parameters[0]) => { - createProject(project) - navigate(`/projects/${project.id}`) + const handleCreateRecollection = useCallback( + (recollection: Parameters[0]) => { + createRecollection(recollection) + navigate(`/recollections/${recollection.id}`) }, - [createProject, navigate] + [createRecollection, navigate] ) return ( @@ -99,7 +97,7 @@ export function KosmosSidebar() {
) : ( - + - - + + - Pleroma - - - - - - - - Keroma + Recollections - {recentProjects.length > 0 && ( + {recentRecollections.length > 0 && ( Recently used - {recentProjects.map((project) => { - const Icon = getProjectIcon(project.iconId) - const isActive = selectedProjectId === project.id + {recentRecollections.map((recollection) => { + const Icon = getRecollectionIcon(recollection.iconId) + const isActive = selectedRecollectionId === recollection.id return ( - + handleSelectProject(project.id)} + onClick={() => handleSelectRecollection(recollection.id)} > - {project.name} + {recollection.name} ) @@ -175,13 +165,13 @@ export function KosmosSidebar() { - p.name)} + p.name)} trigger={ - {orderedProjects.length === 0 ? 'Create your first project' : 'New project'} + {orderedRecollections.length === 0 ? 'Create your first recollection' : 'New recollection'} } /> diff --git a/frontend/src/app/kosmos/NewProjectDialog.tsx b/frontend/src/app/kosmos/NewRecollectionDialog.tsx similarity index 70% rename from frontend/src/app/kosmos/NewProjectDialog.tsx rename to frontend/src/app/kosmos/NewRecollectionDialog.tsx index bd9e12c..80ab51f 100644 --- a/frontend/src/app/kosmos/NewProjectDialog.tsx +++ b/frontend/src/app/kosmos/NewRecollectionDialog.tsx @@ -1,5 +1,5 @@ /** - * Dialog to create a new project: name + icon. + * Dialog to create a new recollection: name + icon. */ import React, { useState } from 'react' @@ -16,20 +16,20 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Plus } from 'lucide-react' -import { PROJECT_ICON_IDS, type Project, type ProjectIconId } from './types' -import { getProjectIcon } from '@/lib/iconMap' +import { RECOLLECTION_ICON_IDS, type Recollection, type RecollectionIconId } from './types' +import { getRecollectionIcon } from '@/lib/iconMap' -type NewProjectDialogProps = { - onCreate: (project: Project) => void +type NewRecollectionDialogProps = { + onCreate: (recollection: Recollection) => void trigger?: React.ReactNode - /** Other project names to check for duplicates (case-insensitive warning only) */ + /** Other recollection names to check for duplicates (case-insensitive warning only) */ existingNames?: string[] } -export function NewProjectDialog({ onCreate, trigger, existingNames = [] }: NewProjectDialogProps) { +export function NewRecollectionDialog({ onCreate, trigger, existingNames = [] }: NewRecollectionDialogProps) { const [open, setOpen] = useState(false) const [name, setName] = useState('') - const [iconId, setIconId] = useState('cat') + const [iconId, setIconId] = useState('cat') const trimmed = name.trim() const isDuplicate = trimmed.length > 0 && existingNames.some((n) => n.toLowerCase() === trimmed.toLowerCase()) @@ -37,13 +37,13 @@ export function NewProjectDialog({ onCreate, trigger, existingNames = [] }: NewP const handleSubmit = (e: React.FormEvent) => { e.preventDefault() if (!trimmed) return - const project: Project = { - id: `proj_${Date.now()}`, + const recollection: Recollection = { + id: `recollection_${Date.now()}`, name: trimmed, iconId, createdAt: Date.now(), } - onCreate(project) + onCreate(recollection) setName('') setIconId('cat') setOpen(false) @@ -55,41 +55,41 @@ export function NewProjectDialog({ onCreate, trigger, existingNames = [] }: NewP {trigger ?? ( )}
- New project - Create a project to start editing a graph canvas. + New recollection + Create a recollection to start editing a graph canvas.
-
- setIconId(v as RecollectionIconId)}> - {PROJECT_ICON_IDS.map((id) => { - const Icon = getProjectIcon(id) + {RECOLLECTION_ICON_IDS.map((id) => { + const Icon = getRecollectionIcon(id) return ( diff --git a/frontend/src/app/kosmos/types.ts b/frontend/src/app/kosmos/types.ts index 51eb67d..65bdd49 100644 --- a/frontend/src/app/kosmos/types.ts +++ b/frontend/src/app/kosmos/types.ts @@ -1,21 +1,21 @@ /** - * Platform types: projects and sidebar state. + * Platform types: recollections and sidebar state. */ import type { LucideIcon } from 'lucide-react' -export type Project = { +export type Recollection = { id: string name: string - /** Icon identifier: key of PROJECT_ICONS map */ + /** Icon identifier: key of RECOLLECTION_ICONS map */ iconId: string createdAt: number - /** Last time the project was opened/edited; used for sorting. Defaults to createdAt if missing. */ + /** Last time the recollection was opened/edited; used for sorting. Defaults to createdAt if missing. */ lastEditedAt?: number } -/** Project icon ids: Lucide "Animals" category only */ -export const PROJECT_ICON_IDS = [ +/** Recollection icon ids: Lucide "Animals" category only */ +export const RECOLLECTION_ICON_IDS = [ 'bird', 'bug', 'cat', @@ -30,4 +30,4 @@ export const PROJECT_ICON_IDS = [ 'egg' ] as const -export type ProjectIconId = (typeof PROJECT_ICON_IDS)[number] +export type RecollectionIconId = (typeof RECOLLECTION_ICON_IDS)[number] diff --git a/frontend/src/app/pleroma/projectGraphStorage.ts b/frontend/src/app/pleroma/projectGraphStorage.ts deleted file mode 100644 index 9967772..0000000 --- a/frontend/src/app/pleroma/projectGraphStorage.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Per-project graph persistence (localStorage). - * Saves and loads StoredGraphState (version + nodes + edges). Used by useCanvasGraph and export. - */ - -import type { StoredGraphState } from '@/lib/graph/state' - -export type { StoredGraphState } -export const PROJECT_FILE_EXT = '.zui.json' -export const PROJECT_VERSION = 1 - -const GRAPH_KEY_PREFIX = 'zui_graph_' - -export function getGraphStorageKey(projectId: string): string { - return `${GRAPH_KEY_PREFIX}${projectId}` -} - -export function loadGraphFromStorage(projectId: string): StoredGraphState | null { - try { - const raw = localStorage.getItem(getGraphStorageKey(projectId)) - if (!raw) return null - const data = JSON.parse(raw) as unknown - if (!data || typeof data !== 'object' || !Array.isArray((data as StoredGraphState).nodes) || !Array.isArray((data as StoredGraphState).edges)) - return null - return data as StoredGraphState - } catch { - return null - } -} - -export function saveGraphToStorage(projectId: string, state: StoredGraphState): void { - localStorage.setItem(getGraphStorageKey(projectId), JSON.stringify(state)) -} - -export function removeGraphFromStorage(projectId: string): void { - localStorage.removeItem(getGraphStorageKey(projectId)) -} diff --git a/frontend/src/app/pleroma/PleromaPage.tsx b/frontend/src/app/recollections/RecollectionsPage.tsx similarity index 80% rename from frontend/src/app/pleroma/PleromaPage.tsx rename to frontend/src/app/recollections/RecollectionsPage.tsx index 43d7b64..fdebf09 100644 --- a/frontend/src/app/pleroma/PleromaPage.tsx +++ b/frontend/src/app/recollections/RecollectionsPage.tsx @@ -1,5 +1,5 @@ /** - * Projects list page: table or cards view with search, sort, pagination, actions. + * Recollections list page: table or cards view with search, sort, pagination, actions. * Sort by last edited (default), name, or created; configurable page size; export toast; undo delete. */ @@ -56,18 +56,18 @@ import { } from 'lucide-react' import { Checkbox } from '@/components/ui/checkbox' import { usePlatform } from '@/app/kosmos/KosmosContext' -import { getProjectIcon } from '@/lib/iconMap' +import { getRecollectionIcon } from '@/lib/iconMap' import { loadGraphFromStorage, saveGraphToStorage, removeGraphFromStorage, - PROJECT_FILE_EXT, - PROJECT_VERSION, -} from './projectGraphStorage' + RECOLLECTION_FILE_EXT, + RECOLLECTION_VERSION, +} from './recollectionGraphStorage' import { toast } from 'sonner' -import { NewProjectDialog } from '@/app/kosmos/NewProjectDialog' -import { ProjectsPageBackground } from './ProjectsPageBackground' -import type { Project } from '@/app/kosmos/types' +import { NewRecollectionDialog } from '@/app/kosmos/NewRecollectionDialog' +import { RecollectionsPageBackground } from './RecollectionsPageBackground' +import type { Recollection } from '@/app/kosmos/types' const PAGE_SIZE_OPTIONS = [10, 25, 50] as const type SortKey = 'lastEdited' | 'name' | 'created' @@ -98,8 +98,8 @@ function getRelativeTime(ts: number): string { return 'Just now' } -function getGraphCounts(projectId: string): { nodes: number; edges: number } { - const stored = loadGraphFromStorage(projectId) +function getGraphCounts(recollectionId: string): { nodes: number; edges: number } { + const stored = loadGraphFromStorage(recollectionId) if (!stored) return { nodes: 0, edges: 0 } return { nodes: Array.isArray(stored.nodes) ? stored.nodes.length : 0, @@ -110,7 +110,7 @@ function getGraphCounts(projectId: string): { nodes: number; edges: number } { type NodeLike = { id: string; position?: { x: number; y: number } } type EdgeLike = { id?: string; source: string; target: string } -/** Shared grid style for empty thumbnail, non-empty thumbnail SVG, and New project placeholder. */ +/** Shared grid style for empty thumbnail, non-empty thumbnail SVG, and New recollection placeholder. */ const THUMBNAIL_GRID = { baseFill: 'hsl(var(--muted) / 0.4)', dotFill: 'hsl(var(--muted-foreground) / 0.06)', @@ -124,13 +124,13 @@ const thumbnailGridStyle: React.CSSProperties = { } /** Renders a minimal SVG preview of the graph from storage, or a placeholder. */ -function GraphThumbnail({ projectId, className }: { projectId: string; className?: string }) { - const stored = loadGraphFromStorage(projectId) +function GraphThumbnail({ recollectionId, className }: { recollectionId: string; className?: string }) { + const stored = loadGraphFromStorage(recollectionId) const nodes = (stored?.nodes ?? []) as NodeLike[] const edges = (stored?.edges ?? []) as EdgeLike[] const withPos = nodes.filter((n) => n.position && typeof n.position.x === 'number' && typeof n.position.y === 'number') - const dotGridPatternId = `dotgrid-${projectId.replace(/\W/g, '-')}` + const dotGridPatternId = `dotgrid-${recollectionId.replace(/\W/g, '-')}` if (withPos.length === 0) { return ( @@ -207,47 +207,47 @@ function GraphThumbnail({ projectId, className }: { projectId: string; className ) } -type ProjectActionsMenuProps = { - project: Project +type RecollectionActionsMenuProps = { + recollection: Recollection onOpen: (id: string) => void - onRenameOpen: (project: Project) => void - onDuplicateOpen: (project: Project) => void - onExport: (project: Project) => void - onDeleteOpen: (project: Project) => void + onRenameOpen: (recollection: Recollection) => void + onDuplicateOpen: (recollection: Recollection) => void + onExport: (recollection: Recollection) => void + onDeleteOpen: (recollection: Recollection) => void trigger: React.ReactNode } -function ProjectActionsMenu({ - project, +function RecollectionActionsMenu({ + recollection, onOpen, onRenameOpen, onDuplicateOpen, onExport, onDeleteOpen, trigger, -}: ProjectActionsMenuProps) { +}: RecollectionActionsMenuProps) { return ( {trigger} - onOpen(project.id)}> + onOpen(recollection.id)}> Open - onRenameOpen(project)}> + onRenameOpen(recollection)}> Rename - onDuplicateOpen(project)}> + onDuplicateOpen(recollection)}> Duplicate - onExport(project)}> + onExport(recollection)}> Export - onDeleteOpen(project)}> + onDeleteOpen(recollection)}> Delete @@ -258,8 +258,8 @@ function ProjectActionsMenu({ export type ViewMode = 'table' | 'cards' -export function ProjectsPage() { - const { orderedProjects, deleteProject, renameProject, createProject, restoreProject } = usePlatform() +export function RecollectionsPage() { + const { orderedRecollections, deleteRecollection, renameRecollection, createRecollection, restoreRecollection } = usePlatform() const navigate = useNavigate() const [viewMode, setViewMode] = useState('cards') const [search, setSearch] = useState('') @@ -267,21 +267,21 @@ export function ProjectsPage() { const [page, setPage] = useState(0) const [sortKey, setSortKey] = useState('lastEdited') const [sortDir, setSortDir] = useState('desc') - const [renameTarget, setRenameTarget] = useState(null) + const [renameTarget, setRenameTarget] = useState(null) const [renameValue, setRenameValue] = useState('') const renameInputRef = React.useRef(null) - const [deleteTarget, setDeleteTarget] = useState(null) - const [duplicateTarget, setDuplicateTarget] = useState(null) + const [deleteTarget, setDeleteTarget] = useState(null) + const [duplicateTarget, setDuplicateTarget] = useState(null) const [duplicateName, setDuplicateName] = useState('') const duplicateInputRef = React.useRef(null) const [selectedIds, setSelectedIds] = useState>(new Set()) - const [bulkDeleteTargets, setBulkDeleteTargets] = useState(null) + const [bulkDeleteTargets, setBulkDeleteTargets] = useState(null) const filtered = useMemo(() => { const q = search.trim().toLowerCase() - if (!q) return orderedProjects - return orderedProjects.filter((p) => p.name.toLowerCase().includes(q)) - }, [orderedProjects, search]) + if (!q) return orderedRecollections + return orderedRecollections.filter((p) => p.name.toLowerCase().includes(q)) + }, [orderedRecollections, search]) const sorted = useMemo(() => { const arr = [...filtered] @@ -312,15 +312,15 @@ export function ProjectsPage() { } const handleOpen = useCallback( - (projectId: string) => { - navigate(`/projects/${projectId}`) + (recollectionId: string) => { + navigate(`/recollections/${recollectionId}`) }, [navigate] ) - const handleRenameOpen = useCallback((project: Project) => { - setRenameTarget(project) - setRenameValue(project.name) + const handleRenameOpen = useCallback((recollection: Recollection) => { + setRenameTarget(recollection) + setRenameValue(recollection.name) }, []) React.useEffect(() => { @@ -339,47 +339,47 @@ export function ProjectsPage() { const handleRenameSubmit = useCallback(() => { if (renameTarget && renameValue.trim()) { - renameProject(renameTarget.id, renameValue.trim()) + renameRecollection(renameTarget.id, renameValue.trim()) setRenameTarget(null) setRenameValue('') } - }, [renameTarget, renameValue, renameProject]) + }, [renameTarget, renameValue, renameRecollection]) - const handleDeleteOpen = useCallback((project: Project) => { - setDeleteTarget(project) + const handleDeleteOpen = useCallback((recollection: Recollection) => { + setDeleteTarget(recollection) }, []) const handleDeleteConfirm = useCallback(() => { if (!deleteTarget) return - const project = deleteTarget - const graphSnapshot = loadGraphFromStorage(project.id) + const recollection = deleteTarget + const graphSnapshot = loadGraphFromStorage(recollection.id) const snapshot = graphSnapshot && (graphSnapshot.nodes.length > 0 || graphSnapshot.edges.length > 0) ? { nodes: graphSnapshot.nodes, edges: graphSnapshot.edges } : null - removeGraphFromStorage(project.id) - deleteProject(project.id) + removeGraphFromStorage(recollection.id) + deleteRecollection(recollection.id) setDeleteTarget(null) - navigate('/projects', { replace: true }) - toast(`"${project.name}" deleted`, { + navigate('/recollections', { replace: true }) + toast(`"${recollection.name}" deleted`, { action: { label: 'Undo', - onClick: () => restoreProject(project, snapshot), + onClick: () => restoreRecollection(recollection, snapshot), }, duration: 8000, }) - }, [deleteTarget, deleteProject, navigate, restoreProject]) + }, [deleteTarget, deleteRecollection, navigate, restoreRecollection]) const handleExport = useCallback( - (project: Project) => { - const stored = loadGraphFromStorage(project.id) + (recollection: Recollection) => { + const stored = loadGraphFromStorage(recollection.id) const state = stored - ? { version: PROJECT_VERSION, nodes: stored.nodes, edges: stored.edges } - : { version: PROJECT_VERSION, nodes: [], edges: [] } + ? { version: RECOLLECTION_VERSION, nodes: stored.nodes, edges: stored.edges } + : { version: RECOLLECTION_VERSION, nodes: [], edges: [] } const blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') - const filename = `${project.name.replace(/[^\w.-]/g, '_')}${PROJECT_FILE_EXT}` + const filename = `${recollection.name.replace(/[^\w.-]/g, '_')}${RECOLLECTION_FILE_EXT}` a.href = url a.download = filename a.click() @@ -389,40 +389,40 @@ export function ProjectsPage() { [] ) - const handleCreateProject = useCallback( - (project: Project) => { - createProject(project) - navigate(`/projects/${project.id}`) + const handleCreateRecollection = useCallback( + (recollection: Recollection) => { + createRecollection(recollection) + navigate(`/recollections/${recollection.id}`) }, - [createProject, navigate] + [createRecollection, navigate] ) - const handleDuplicateOpen = useCallback((project: Project) => { - setDuplicateTarget(project) - setDuplicateName(`${project.name} (copy)`) + const handleDuplicateOpen = useCallback((recollection: Recollection) => { + setDuplicateTarget(recollection) + setDuplicateName(`${recollection.name} (copy)`) }, []) const handleDuplicateConfirm = useCallback(() => { if (!duplicateTarget || !duplicateName.trim()) return const name = duplicateName.trim() - const newId = `proj_${Date.now()}_${Math.random().toString(36).slice(2, 6)}` + const newId = `recollection_${Date.now()}_${Math.random().toString(36).slice(2, 6)}` const now = Date.now() - const newProject: Project = { + const newRecollection: Recollection = { id: newId, name, iconId: duplicateTarget.iconId, createdAt: now, lastEditedAt: now, } - createProject(newProject) + createRecollection(newRecollection) const graph = loadGraphFromStorage(duplicateTarget.id) if (graph && (graph.nodes.length > 0 || graph.edges.length > 0)) { - saveGraphToStorage(newId, { version: PROJECT_VERSION, nodes: graph.nodes, edges: graph.edges }) + saveGraphToStorage(newId, { version: RECOLLECTION_VERSION, nodes: graph.nodes, edges: graph.edges }) } - toast.success('Project duplicated') + toast.success('Recollection duplicated') setDuplicateTarget(null) setDuplicateName('') - }, [duplicateTarget, duplicateName, createProject]) + }, [duplicateTarget, duplicateName, createRecollection]) const allOnPageSelected = pageItems.length > 0 && pageItems.every((p) => selectedIds.has(p.id)) const someOnPageSelected = pageItems.some((p) => selectedIds.has(p.id)) @@ -462,33 +462,33 @@ export function ProjectsPage() { const handleBulkDeleteConfirm = useCallback(() => { if (!bulkDeleteTargets || bulkDeleteTargets.length === 0) return const count = bulkDeleteTargets.length - bulkDeleteTargets.forEach((project) => { - removeGraphFromStorage(project.id) - deleteProject(project.id) + bulkDeleteTargets.forEach((recollection) => { + removeGraphFromStorage(recollection.id) + deleteRecollection(recollection.id) }) setBulkDeleteTargets(null) setSelectedIds(new Set()) - navigate('/projects', { replace: true }) - toast(`${count} project${count === 1 ? '' : 's'} deleted`) - }, [bulkDeleteTargets, deleteProject, navigate]) + navigate('/recollections', { replace: true }) + toast(`${count} recollection${count === 1 ? '' : 's'} deleted`) + }, [bulkDeleteTargets, deleteRecollection, navigate]) const handleBulkExport = useCallback(() => { const toExport = sorted.filter((p) => selectedIds.has(p.id)) - toExport.forEach((project) => { - const stored = loadGraphFromStorage(project.id) + toExport.forEach((recollection) => { + const stored = loadGraphFromStorage(recollection.id) const state = stored - ? { version: PROJECT_VERSION, nodes: stored.nodes, edges: stored.edges } - : { version: PROJECT_VERSION, nodes: [], edges: [] } + ? { version: RECOLLECTION_VERSION, nodes: stored.nodes, edges: stored.edges } + : { version: RECOLLECTION_VERSION, nodes: [], edges: [] } const blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') - const filename = `${project.name.replace(/[^\w.-]/g, '_')}${PROJECT_FILE_EXT}` + const filename = `${recollection.name.replace(/[^\w.-]/g, '_')}${RECOLLECTION_FILE_EXT}` a.href = url a.download = filename a.click() URL.revokeObjectURL(url) }) - toast.success(`Exported ${toExport.length} project${toExport.length === 1 ? '' : 's'}`) + toast.success(`Exported ${toExport.length} recollection${toExport.length === 1 ? '' : 's'}`) }, [sorted, selectedIds]) const SortIcon = ({ columnKey }: { columnKey: SortKey }) => { @@ -498,7 +498,7 @@ export function ProjectsPage() { return (
- +
@@ -511,14 +511,14 @@ export function ProjectsPage() {
{ setSearch(e.target.value) setPage(0) }} className="h-9 w-full pl-8" - aria-label="Search projects by name" + aria-label="Search recollections by name" />
@@ -562,9 +562,9 @@ export function ProjectsPage() { {sorted.length === 0 ? (
- p.name)} + p.name)} trigger={
- New project + New recollection - {search.trim() ? 'No projects match your search.' : 'Create a new project'} + {search.trim() ? 'No recollections match your search.' : 'Create a new recollection'}
@@ -663,42 +663,42 @@ export function ProjectsPage() { - {pageItems.map((project) => { - const Icon = getProjectIcon(project.iconId) - const counts = getGraphCounts(project.id) - const lastEdited = project.lastEditedAt ?? project.createdAt - const isSelected = selectedIds.has(project.id) + {pageItems.map((recollection) => { + const Icon = getRecollectionIcon(recollection.iconId) + const counts = getGraphCounts(recollection.id) + const lastEdited = recollection.lastEditedAt ?? recollection.createdAt + const isSelected = selectedIds.has(recollection.id) return ( handleOpen(project.id)} + onClick={() => handleOpen(recollection.id)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() - handleOpen(project.id) + handleOpen(recollection.id) } }} tabIndex={0} role="button" - aria-label={`Open ${project.name}`} + aria-label={`Open ${recollection.name}`} > e.stopPropagation()}> toggleSelection(project.id)} - aria-label={`Select ${project.name}`} + onCheckedChange={() => toggleSelection(recollection.id)} + aria-label={`Select ${recollection.name}`} />
- {project.name} + {recollection.name}
- - {formatDate(project.createdAt)} + + {formatDate(recollection.createdAt)} @@ -716,15 +716,15 @@ export function ProjectsPage() { className={`sticky right-0 w-12 min-w-12 px-1 py-1.5 ${isSelected ? 'bg-muted/70' : 'bg-card group-hover:bg-muted/50'}`} onClick={(e) => e.stopPropagation()} > - + } @@ -741,7 +741,7 @@ export function ProjectsPage() {

Page {page + 1} of {totalPages} - {pageSize !== -1 && ` · ${sorted.length} projects`} + {pageSize !== -1 && ` · ${sorted.length} recollections`}